JavaScript Promises 101
A JavaScript Promise represents the result of an operation that hasn't been completed yet, but will at some undetermined point in the future. An example of such an operation is a network request. When we fetch data from some source, for example an API, there is no way for us to absolutely determine when the response will be received.
This can be problematic if we have other operations dependent on the completion of this network request. Without Promises, we would have to use callbacks to deal with actions that need to happen in sequence. This isn't necessarily a problem if we only have one asynchronous action. But if we need to complete multiple asynchronous steps in sequence, callbacks become unmanageable and result in the infamous callback hell.
doSomething(function(responseOne) {
doSomethingElse(responseOne, function(responseTwo, err) {
if (err) { handleError(err); }
doMoreStuff(responseTwo, function(responseThree, err) {
if (err) { handleAnotherError(err); }
doFinalThing(responseThree, function(err) {
if (err) { handleAnotherError(err); }
// Complete
}); // end doFinalThing
}); // end doMoreStuff
}); // end doSomethingElse
}); // end doSomething
Promises provide a standardised and cleaner method of dealing with tasks that need to happen in sequence.
doSomething()
.then(doSomethingElse)
.catch(handleError)
.then(doMoreStuff)
.then(doFinalThing)
.catch(handleAnotherError)
Creating Promises #
A Promise is created using the Promise Constructor. This accepts a function with two arguments (resolve
& reject
) as its only parameter.
var promise = new Promise( function(resolve, reject) { /* Promise content */ } )
Within the function, we can execute whatever asynchronous task we want. To mark the promise as fulfilled, we call resolve()
, optionally passing a value we want to return. To mark the promise as rejected or failed, we call reject()
, optionally passing an error message. Before a promise is fulfilled or rejected, it is in a pending state.
Here is a common promise-ified version of an XMLHttpRequest -
/* CREDIT - Jake Archibald, https://www.html5rocks.com/en/tutorials/es6/promises/ */
function get(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
resolve(req.response); /* PROMISE RESOLVED */
} else {
reject(Error(req.statusText)); /* PROMISE REJECTED */
}
};
req.onerror = function() { reject(Error("Network Error")); };
req.send();
});
}
Using Promises #
Once we have created the Promise, we need to actually use it. To execute the promise-ified function, we can call it like any regular function. But, because it is a promise, we now have access to the .then()
method, which we can append to the function and which will be executed when the Promise is no longer pending.
The .then()
method accepts two optional parameters. First, a function called if the promise is fulfilled. Second, a function called if the promise is rejected.
get(url)
.then(function(response) {
/* successFunction */
}, function(err) {
/* errorFunction */
})
Handling Errors #
Since both the success and error functions are optional, we can split them into two .then()
s for better readability.
get(url)
.then(function(response) {
/* successFunction */
}, undefined)
.then(undefined, function(err) {
/* errorFunction */
})
To make things even more readable, we make use of the .catch()
method, which is a shorthand for a .then(undefined, errorFunction)
.
get(url)
.then(function(response) {
/* successFunction */
})
.catch(function(err) {
/* errorFunction */
})
Chaining #
The real value in promises is when we have multiple asynchronous functions we need to execute in order. We can chain .then()
and .catch()
together to create a sequence of asynchronous functions.
We do this by returning another promise within a success or error function. For example -
get(url)
.then(function(response) {
response = JSON.parse(response);
var secondURL = response.data.url
return get( secondURL ); /* Return another Promise */
})
.then(function(response) {
response = JSON.parse(response);
var thirdURL = response.data.url
return get( thirdURL ); /* Return another Promise */
})
.catch(function(err) {
handleError(err);
});
If a promise within the chain resolved, it will move on to the next success function (.then()
) in the sequence. If, on the other hand, a promise rejected, it will jump to the next error function (.catch()
) in the sequence.
Executing Promises in Parallel #
There may be cases where we want to execute a bunch of promise-ified functions in parallel, and then perform an action only when all the promises have been fulfilled. For example, if we want to fetch a bunch of images and display them on the page.
To do this, we need to make use of two methods. First, the Array.map()
method allows us to perform an action on each item in an array, and creates a new array of the results of these actions.
Second, the Promise.all()
method returns a promise that is only resolved when every promise within an array is resolved. If any single promise within the array is rejected, the Promise.all()
promise is also rejected.
var arrayOfURLs = ['one.json', 'two.json', 'three.json', 'four.json'];
var arrayOfPromises = arrayOfURLs.map(get);
Promise.all(arrayOfPromises)
.then(function(arrayOfResults) {
/* Do something when all Promises are resolved */
})
.catch(function(err) {
/* Handle error is any of Promises fails */
})
If we look at the Network panel in our Development tools, we can see that all the fetch requests are happening in parallel.
Support #
If you have to support Internet Explorer or Opera Mini, you can make use of this Promise Polyfill.