JavaScript Promises
Promises in JavaScript are a tool for dealing with callbacks. I’ll cover all the basics about promises here, but to understand JavaScript promises it is good to understand the motivation behind them. So we will have a look at how they compare to plain callbacks.
Plain Callbacks
Here is an example of the setTimeOut
callback that I covered in this tutorial:
setTimeout(function(){
alert('5 seconds has passed');
}, 5000);
As a quick recap, setTimeout
will wait for a period of time, such as 5 seconds in the example above, and then call the function you pass in to it. This function is referred to as the callback function.
Now let’s imagine you want to wait another 4 seconds, and do something. What would that code look like? You’d have to call setTimeout
again, but only after the first time out has finished. So your code would look like this:
setTimeout(function(){
alert('5 seconds has passed');
setTimeout(function(){
alert('4 seconds has passed');
}, 4000);
}, 5000);
And if you want a third call, you’d get another level of nesting:
setTimeout(function(){
alert('5 seconds has passed');
setTimeout(function(){
alert('4 seconds has passed');
setTimeout(function(){
alert('3 seconds has passed');
}, 3000);
}, 4000);
}, 5000);
Nesting means functions within functions, which when formatted cause the code to be indented more and more. Above there are 3 levels of nesting, one for each setTimeout
call.
I think that this getting too unwieldy now, having so many levels of nesting. Of course you could tidy this up by having multiple functions:
setTimeout(function(){
alert('5 seconds has passed');
doTimeout2();
}, 5000);
function doTimeout2() {
setTimeout(function(){
alert('4 seconds has passed');
doTimeout3();
}, 4000);
}
function doTimeout3() {
setTimeout(function(){
alert('3 seconds has passed');
}, 3000);
}
However, while this reduces indention it means you have to sprawl your code across many functions, when this might not be the way you want to organize things. It get’s worse when dealing with error cases.
Callbacks For Errors
The setTimeout
uses a single callback. However many functions have 2 (or more) callbacks. There is often one for success and another for failure. For example when using XMLHttpRequest
to make a request to the server, we can set up 2 callbacks, one for load
which means a successful response, and one for error
which means an unexpected error:
var request = new XMLHttpRequest();
// Callback that is called on success:
request.addEventListener("load", function() {
console.log(request.responseText);
});
// Callback that is called on failure:
request.addEventListener("error", function() {
console.log('error');
});
// Make the request to get the data from this url:
request.open("GET", "https://httpbin.org/get");
request.send();
What if you want to call a 3 services, one after the other, and you want to handle all of the possible error cases. What would that code look like? This is what is often called “callback hell” in Javascript, creating tangled, heavily nested code that is hard to read.
There is a solution to this, and as you might have guessed it is JavaScript Promises.
JavaScript Promises Basics
Make a Promise
A promise is an object in JavaScript that “wraps” a callback function, and provides a convenient and consistent way to handle success and failure scenarios.
Here is how you can create a promise from setTimeOut
:
var promise1 = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('timeout elapsed');
}, 1000);
});
Understanding this code can be a bit tricky at first, so let’s go through it slowly.
The Promise
constructor takes a function that will do some work that typically involves a callback. In this case we are using setTimeOut
which will schedule some code to run a bit later.
I will zoom in on the callback function that we pass into the Promise
constructor:
function(resolve, reject) {
setTimeout(function() {
resolve('timeout elapsed');
}, 1000);
};
It has two arguments: resolve
and reject
.
resolve
is a function that you call if what you have done has been successful. It takes as an argument any data that you now have as a result of the success. For example this could be a response from a server to say you have successfully logged in.
reject
is a function that you call if what you have done has failed. It also can take an argument with some data about why it failed.
In the example above, we call resolve
in the callback of setTimeout
. This means that once 1 second has passed, resolve
is called to indicate to the promise that we are finished. We pass in the string 'timeout elapsed'
which can be used by whatever handles the promise.
In this simple case there is no need to call reject
as there is no failure scenario.
Use a Promise
With the promise created above, we can then add a handler for when the callback is complete. We do this by calling the then
method of the promise. The method takes an argument that is the callback function, as shown below:
var promise1 = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('timeout elapsed');
}, 1000);
});
// Using the promise:
promise1.then(function(result) {
console.log(result);
});
The code above will wait for 1 second, and then:
- The
setTimeout
will call it’s call back which resolves the promise. - This causes the
then
callback to be called, which will log theresolve
parameter, which is'timeout elapsed'
, to the console.
Chaining JavaScript Promises
The then
method itself returns a promise, which will resolve once the original promise has resolve and it’s then function has resolved.
This means you can chain another action to happen after the first one, like so:
// Using the promise:
promise1.then(function(result) {
console.log(result);
}).then(function(result) {
console.log('second action');
});
The second action will happen immediately after the first when chained in this way.
Chaining in this way is synchronous, in other words the next action happens immediately after the first one.
Chaining a Promise with another Promise
What if you have two actions that have a call back, and you want to run the first, and once that comes back run the second?
An example of this is logging into a bank, then once logged in fetching your bank balance.
Another example is the one we encountered earlier with this tutorial:
setTimeout(function(){
alert('5 seconds has passed');
setTimeout(function(){
alert('4 seconds has passed');
}, 4000);
}, 5000);
Promises can do this too. To do this you return a new promise in the then
function. When you do this:
- The new promise is started once the first promise successfully completes.
- It’s callback is handled by the next
then
function in the chain.
Ok, I think an example will make it clearer. The code for waiting 4 seconds then 5 seconds can be written like this:
// Function to make a promise that waits for a number
// of seconds, then resolves:
function makePromise(seconds) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(seconds);
}, 1000 * seconds);
});
}
makePromise(5).then(function (){
// This gets run after 5 seconds:
console.log('5 seconds has passed');
// Kicks of a 4 second timer, and the next
// `then` handles the reponse to this new
// promise:
return makePromise(4);
}).then(function () {
// This gets run 4 secods after the first 5
// seconds has elapsed:
console.log('4 seconds has passed');
})
Chaining in this way is asynchronous, in other words the second callback will happen some time later, and other JavaScript code might be run inbetween.
Promises Are Reusable
That previous example seems like a lot of code, but we can fix that.
The beauty of promises is the makePromise
code above can be put into a separate library file, because it is a general purpose promise maker for setTimeout
.
To show this I am going to chain 10 timeouts together, assuming makePromise
is in a library. Notice how there is no nesting like we saw earlier with the callbacks:
// We are doing the same action a few times so I have
// put it in a handy function:
function action() {
console.log('Callback occurred');
return makePromise(1);
}
// Wait a second then peform the action 10 times in a row,
// but each time only happening after the previous promise is
// resolved.
makePromise(1)
.then(action)
.then(action)
.then(action)
.then(action)
.then(action)
.then(action)
.then(action)
.then(action)
.then(action)
.then(action);
The code above will put the words 'Callback occurred'
to the console output ten times, with a one second gap before each time.
Rejected promises
Some actions, such as sending a message to the server, can sometimes fail. For example if the network is disconnected. Promises provide a way to handle those failures.
Earlier I showed this example of using callbacks to handle both success and errors when making a web request:
var request = new XMLHttpRequest();
// Callback that is called on success:
request.addEventListener("load", function() {
console.log(request.responseText);
});
// Callback that is called on failure:
request.addEventListener("error", function() {
console.log('error');
});
// Make the request to get the data from this url:
request.open("GET", "https://httpbin.org/get");
request.send();
Creating a promise that rejects
We can convert this into a promise, by observing that we have one callback that deals with success and another that deals with errors:
var promise = new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
// Callback that is called on success:
request.addEventListener("load", function(result) {
resolve(request, result);
});
// Callback that is called on failure:
request.addEventListener("error", function(result) {
reject(request, result);
});
// Make the request to get the data from this url:
request.open("GET", "https://httpbin.org/get");
request.send();
});
promise.then(function(request){
console.log(request.responseText);
});
It’s really the same code wrapped in a promise. The promise function doesn’t usually handle ‘doing’ things like writing to the console log, this is done instead by the user of the promise. This makes the promise reusable for different situations.
In the example above we don’t handle the failure case, but it is easy to do so. Instead of calling then
we can call catch
:
promise.then(function(request){
console.log(request.responseText);
}).catch(function(request){
console.log('An error occurred');
});
Exception handling in promises
Promises will handle any exceptions thrown in the promise function, and this will also cause the catch
method to be run, with the argument being the error that was caught, for example:
var promise = new Promise(function(resolve, reject) {
throw 'Just an error';
});
promise.then(function(){
console.log('Success');
}).catch(function(ex){
console.log('An error occurred: ' + ex);
});
// Result: An error occurred: Just an error
Summary
This tutorial covers JavaScript promises, that provide a neat and consistent way to handle callbacks. They can be chained together and allow you to provide handlers for both success and error outcomes.