Promises
Promise
Asynchronous programming in modern JavaScript revolves around promises.
A Promise
object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
A Promise
object can be created using the Promise
constructor.
The constructor takes an argument being the executor function ((resolve, reject) => {...}
) that handles the asynchronous operation, i.e., the "event".
The details about the executor will be explained further in this section.
After the asynchronous operation completed (or failed) an event handler is called to handle the result of the asynchronous event.
This is taken care of by a then()
method, which will be explained in the next section.
const myPromise = new Promise((resolve, reject) => {
// do something asynchronous which eventually calls either
// resolve(someValue) or reject("failure reason"):
const img = document.createElement('img');
img.src = "https://art.fridoverweij.com/images/Girl_with_a_Pearl_Earring-420px.jpg";
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Image loading failed"));
});
The executor generally does something asynchronous but this is not mandatory:
resolve
or reject
can also be called immediately.
Earlier
we used a general loadImg
function that we can pass to a given image to load.
We can adjust this function so that it returns the promise.
function loadImg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement('img');
img.src = src;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Image loading failed for ${src}`));
});
};
Creating a promise automatically invokes the executor function. The executor starts the event, i.e., the asynchronous operation,
loading the image (by initializing the src
attribute) in the example above.
When the promise is created it also automatically generates two functions: a resolution function and a rejection function.
These two functions should be passed into the executor. In the example above they are given the names resolve
and reject
.
You need to have the executer function call the resolution function in case the event finished successfully and
call the rejection function in case the event failed.
The initial "state" of the promise is "pending" until eventually it is settled. When the promise was settled its state changed to either fulfilled (aka resolved) or rejected. On fulfillment, the executer calls the resolution function, on rejection, the executer calls the rejection function. Either the (first) resolution function or the (first) rejection function is called, any possible further call of a resolution or rejection function will be ignored.
The resolution function and the rejection function both accept a single argument of any type. Typically the result of the event (or part of it) is passed to the resolution function. Typically a reason for the failure, through an error object, is passed to the rejection function.
then()
& event handlers
We are still missing the event handlers.
The event handlers are defined in the then()
method of the promise.
myPromise.then(resolutionHandler, rejectionHandler);
PS: The rejection handler is optional.
The resolution function or rejection function in the executer calls
its corresponding event handler in the then()
method, while passing its arguments.
In the example below, argument img
in resolve(img)
will be passed to parameter result
of the first event handler,
and the error object argument of the rejection function will be passed to parameter error
of the second event handler.
const myPromise = new Promise((resolve, reject) => {
const img = document.createElement('img');
img.src = "images/Girl_with_a_Pearl_Earring-420px.jpg";
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Image loading failed"));
}).then(
(result) => { console.log(`Image size: ${result.width}x${result.height}`) },
(error) => { console.log(error) }
);
// logs: "Image size: 420x560"
Or with a general loadImg
function:
function loadImg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement('img');
img.src = src;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Image loading failed for &{src}`));
});
};
const imagePromise = loadImg("images/Girl_with_a_Pearl_Earring-420px.jpg");
imagePromise.then(
(result) => { console.log(`Image size: ${result.width}x${result.height}`) },
(error) => { console.log(error) }
);
// logs: "Image size: 420x560"
imagePromise.then(
(result) => { document.getElementById('imgcontainer').append(result); },
(error) => { console.log(error) }
);
In the above example we called the then()
method twice on the same promise, i.e., multiple event handlers on the same loaded image.
This is not possible with only using callbacks.
With the asynchronous callback-based approach,
as covered in the previous chapter,
we loaded the image and called the event handler(s) (via the callback) in the same loadImg
function.
fetch()
In the image loading examples above we (implicitly, by assigning the src
attribute) used an HTTP requests for the image using the
XMLHttpRequest
API.
The asynchronous .onload
and .onerror
events are part of this API.
A modern, more powerful and flexible way to request a resource (i.e. download a file) than using XMLHttpRequest
is using the fetch()
global method.
The fetch()
method returns a Promise
object.
The resolution function of this promise passes a Response
object, in other words:
the promise resolves with a Response
object.
The Response
object is an interface of the Fetch API.
fetch("https://www.somesite.com/some_resource")
.then(
(response) => {
// body of the resolution event handler
});
The rejection function will not be called on a HTTP error status (e.g. 404 file not found or 500 Internal Server Error).
However, the response.ok
property will be set to false
.
The promise will only reject if there was no response at all or if there was an "empty" response (no headers).
fetch("https://www.somesite.com/some_resource")
.then(
(response) => {
if (!response.ok) {
throw new Error('Error in network response');
}
// rest of the body of the resolution event handler
},
(error) => { console.log(error) }
);
Extracting the resource from the Response
object
The Response
object does not directly contain the actual requested file but is instead a representation of the entire HTTP response.
Generally, the actual resource needs to be extracted from the body content from the Response
object.
This extraction is also asynchronous.
In the next example below, the JSON body content is extracted from the downloaded JSON file (using response.json()
).
Extracting content from a plain text file or from an HTML or SVG file can be done by using
response.text()
.
In the example further down below
an actual image is extracted from the response body data of a downloaded image.
Chained promises
The then
method itself returns a promise which resolves with the result of its resolution handler, or reject with the result of the rejection handler.
If the resolution handler throws an error, the promise will reject with this error.
This allows us to chain a number of successive asynchronous functions.
The next example downloads a JSON file. Method response.json()
is used to extract the JSON body content from the Response
object.
Method response.json()
returns a promise which resolves with the result of parsing the response body text as JSON.
The promise returned by the first then
resolves with the promise returned from response.json()
,
that, in turn, resolves with the result of parsing the response body text as JSON (data
in the example below).
The second then
calls a function on that promise (data
) returned by the first then
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
.then((response) => response.json())
.then((data) => console.log(data),
(error) => console.error(error)
);
If then
has no resolution handler, the result will simply be passed through to the next then
resolution handler.
Likewise, if then
has no rejection handler, the error will simply be passed through to the next then
rejection handler.
If, in the example above, the fetch throws an error, the error will be passed through the first then
to the last then
and handled there by the rejection handler.
The next example does exactly the same as the previous example.
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
.then()
.then((response) => response.json())
.then()
.then((data) => console.log(data),
(error) => console.error(error)
);
catch()
& finally()
catch()
Because of this chaining of promises we can rewrite the above example as:
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
.then((response) => response.json())
.then((data) => console.log(data))
.then(undefined, (error) => console.error(error));
Any possible error in the chain rejects all next promises and is passed on to the last then
. The then
method requires a resolution handler,
but in the last then
we are only handling the possible error so it is set to undefined
.
In line with the try...catch
syntax we can replace the last then
in the above example with a
Promise.catch()
method: .catch((error) => console.error(error))
will do exactly the same.
The catch()
method deals with rejected cases only.
A catch()
at the end of a promise chain will be called when any of the consecutive asynchronous operations failed.
A complete code to download a JSON file:
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
})
.catch((error) => {
console.error(error);
});
// logs: "baked beans"
Note that a possible error thrown in the first then
will also bubble down to the catch()
.
Compared to the nested callbacks, the "pyramid of doom" from the previous chapter, we now have a single place to handle all errors and no ever-increasing levels of indentation.
Also the catch
method returns a promise which resolves with the result of the resolution handler (undefined
)
or rejects with the returned error handling (which happens when something goes wrong with the error handling itself).
So we might continue the promise chain with a new then
after a catch
:
the process continues, only after possible previous errors have been handled by the catch
handler.
finally()
Suppose we add another then
method after the catch
. This final then
we give two equal callbacks.
Then this last then
's handler will always be executed.
let isLoading = true;
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
.then((response) => response.json())
.then((data) => console.log(data[0].name))
.catch((error) => console.error(error))
.then(
() => {isLoading = false; console.log(isLoading)},
() => {isLoading = false; console.log(isLoading)}
);
// logs:
// "baked beans"
// false // even if an error occurred, which is correct; the loading did stop, either successfully or with an error.
In line with the try...catch...finally
syntax we can replace the last then
in the above example with a
Promise.finally()
method:
let isLoading = true;
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
.then((response) => response.json())
.then((data) => console.log(data[0].name))
.catch((error) => console.error(error))
.finally(() => {isLoading = false; console.log(isLoading)});
// logs:
// "baked beans"
// false
However, then
with two equal callbacks and a finally
do not act exactly the same:
-
A
finally
handler will not receive any argument. A parameter of thefinally
method will always remainundefined
, contrary to parameters in handlers of athen
method. This is conform the function of afinally
: the passed in rejection reason or fulfillment value are irrelevant since the handler is to always perform its task. -
A
finally
call will chain through an equivalent to the receiving promise, one that resolves or rejects with the same result or error, like athen
without handlers would do. However, afinally
does have a handler, but what it returns has no effect on how the returned promise resolves, except when it throws an error. A failingfinally
handler will reject the returned promise. This all means that afinally
does not have to to be the last in the promise chain; it can be anywhere in the chain, but its position in the chain does affect the outcome of the entire promise chain. In fact, it may be desirable to put thefinally
before acatch
to make sure that a possiblefinally
handler error gets handled.
Example - Downloading an image
Previously we implicitly downloaded, that is, copied an image from some (remote) server and
"cached"
it (stored it inside the browser), by assigning a
URL to the img
's src
attribute. We used the asynchronous onload
to notify the program that loading finished and
to provide a function to do something with the result.
img.src = "images/Girl_with_a_Pearl_Earring-420px.jpg";
img.onload = () => {
console.log(`Image size: ${img.width}x${img.height}`); // logs: "Image size: 420x560"
}
The fetch()
method returns a Promise
object that resolves with a Response
object that
is a representation of the entire HTTP response. We cannot assign that response to the img
's src
attribute. The actual image first needs to be extracted from the response body data (using response.blob()
)
and a temporary internal URL that points to this extraction stored inside the browser needs to be assigned to the img
's src
attribute (using URL.createObjectURL()
).
const img = new Image();
fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((myBlob) => {
img.src = URL.createObjectURL(myBlob);
document.querySelector('body').append(img);
URL.revokeObjectURL(img.src);
})
.catch((error) => {
console.error('Something went wrong when loading the image:', error);
});
The response body data of a fetched image is of a BLOB data type.
Method response.blob()
returns a promise which resolves with the result of parsing the response body data as a
Blob
. This Blob
needs to be assigned to the img
's src
attribute as a URL.
Method
URL.createObjectURL()
creates an Object URL, which is a unique, internal URL that
only exists within the current document and only while the document is open.
Once the image is appended to the document and the Object URL and associated Blob
are no longer needed,
the Object URL is released using URL.revokeObjectURL()
for optimum memory management.
However, assigning an Object URL to the img
's src
attribute still triggers an asynchronous load,
although it is a very fast one from local cache. Retrieving the image's dimensions using img.width
and img.height
will not work unless it is done within a onload
callback function.
const img = new Image();
fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((myBlob) => {
img.src = URL.createObjectURL(myBlob);
document.querySelector('body').append(img);
img.onload = () => {
console.log(`Image size: ${img.width}x${img.height}`);
URL.revokeObjectURL(img.src);
}
})
.catch((error) => {
console.error('Something went wrong when loading the image:', error);
});
// logs: "Image size: 700x658"
An other way is to use global method
createImageBitmap()
that returns a Promise
which resolves to an object that represents the image as a bitmap which can be drawn to a HTML <canvas>
.
This object has two properties: width
and height
(both return in CSS pixels).
const img = new Image();
fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((myBlob) => {
img.src = URL.createObjectURL(myBlob);
document.querySelector('body').append(img);
return createImageBitmap(myBlob);
})
.then((ImageBitmap) => {
console.log(`Image size: ${ImageBitmap.width}x${ImageBitmap.height}`);
})
.catch((error) => {
console.error('Something went wrong when loading the image:', error);
})
.finally(() => URL.revokeObjectURL(img.src));
// logs: "Image size: 700x658"
Return inside an asynchronous function
Suppose we want to do something asynchronous with the image after the onload
and for that we want to pass the image to the next then
by returning it:
const img = new Image();
fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((myBlob) => {
img.src = URL.createObjectURL(myBlob);
img.onload = () => {
return img;
}
})
.then((image) => {
console.log(`Image size: ${image.width}x${image.height}`);
})
.catch((error) => {
console.error('Something went wrong when loading the image:', error);
})
.finally(() => URL.revokeObjectURL(img.src));
// logs: "Something went wrong when loading the image: TypeError: image is undefined"
This does not work because the image is returned from the onload
callback into the resolution handler
and not returned by the resolution handler itself to the next then
.
We can fix this by returning the image by a resolution function of a new promise returned by the resolution handler:
const img = new Image();
fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((myBlob) => new Promise((resolve, reject) => {
img.src = URL.createObjectURL(myBlob);
img.onload = () => {
resolve(img);
}
}))
.then((image) => {
console.log(`Image size: ${image.width}x${image.height}`);
})
.catch((error) => {
console.error('Something went wrong when loading the image:', error);
})
.finally(() => URL.revokeObjectURL(img.src));
// logs: "Image size: 700x658"
Combining promises
A promise chain handles multiple asynchronous functions that depend on each other and each one needs to complete before the next starts. Sometimes multiple promises that do not depend on each other need to be handled by the same resolution and rejection handlers.
The Promise API provides a few methods to handle sets of promises.
One of them is the Promise.all()
method.
Promise.all()
takes an array of promises and returns a single promise that resolves when all the promises in the array are resolved
and it resolves with an array of all the responses in the same order as the promises in the array.
The returned promise rejects when any of the promises in the array is rejected and it rejects with the error thrown by the promise that rejected.
In the next example the second resource does not exist. The server returns 404 (Not Found) instead of 200 (OK).
Promise.all([
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json'),
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found'),
fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json')
])
.then((responses) => {
for (const response of responses) {
if (!response.ok) {
throw new Error(`Error in network response: ${response.status} for fetch ${response.url}`);
}
}
return responses
})
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}`);
}
})
.catch((error) => {
console.error(`Failed to fetch: ${error}`)
});
// logs: "Failed to fetch: Error: Error in network response: 404 for fetch https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found"
Promise.any()
is similar to Promise.all()
except that the returned promise resolves as soon as any of the array of promises is resolved, or
rejects when all of the array of promises are rejected.
Promise.race()
returns a promise which resolves as soon as any of the array of promises is either resolved or rejected.