Beginner's Guide to JavaScript promises
Created: Sept 21st, 2015
JavaScript Promises are a new addition to ECMAscript 6 that aims
to provide a cleaner, more intuitive way to deal with the completion (or
failure) of asynchronous tasks. Up until JavaScript Promises, that job is taken
on by JavaScript event handlers (ie: image.onload
) and callback
functions, popularized by libraries such as jQuery and Node.js, with varying
degrees of frustration. Event handlers work well with individual elements, but
what if you wanted to, for example, be notified when a collection of images have
all been loaded or the order in which they happened? Call back functions that
you pass as the last parameter to methods that support it such as jQuery's
animate()
function perform their job admirably for running custom code
when a task is complete, but what if the custom code in itself also needs to
call
animate()
with another call back function, and so on? You end up with
what's called "callback hell", or a growing stack of call back functions
resembling the Tower of Babel.
JavaScript Promises provide a mechanism for tracking the state of an asynchronous task with more robustness and less chaos. But first thing's first.
JavaScript Promises support and Polyfill
JavaScript Promises are part of the ECMAscript 6 standards and should be supported by all browsers eventually. At the moment that promise is already realized in recent versions of Chrome, FF, Safari, and on mobile browsers with the exception of IE. Check out this page for the skinny. Due to the absence of IE including IE11 in the green column, you can use a Polyfill such as es6-promise.js to bridge the gap until IE catches up with the rest of the herd. Just download es6-promise-min.js and include it at the top of your page as an external JavaScript, and viola!
The syntax
Ok, lets get down to business now. At the heart of JavaScript Promises is the Promise constructor function, which is called like so:
var mypromise = new Promise(function(resolve, reject){ // asynchronous code to run here // call resolve() to indicate task successfully completed // call reject() to indicate task has failed })
It is passed an anonymous function with two parameters- a
resolve()
method that you call at some point to set the state of the promise
to fulfilled, and reject()
to set it to rejected
instead. A Promise object starts out with a state of pending, to
indicate the asynchronous code it's monitoring has neither completed (fulfilled)
or failed (rejected). Lets get our first taste of JavaScript Promises in action
with a function that dynamically loads an image based on the image URL:
function getImage(url){ return new Promise(function(resolve, reject){ var img = new Image() img.onload = function(){ resolve(url) } img.onerror = function(){ reject(url) } img.src = url }) }
The getImage()
function returns a Promise object that keeps
track of the state of the image load. When you call:
getImage('doggy.gif')
its promise object goes from the initial state of "pending" to
either fulfilled or rejected eventually depending on the outcome of the image
load. Notice how we've passed the URL of the image to both the resolve()
and
reject()
method of Promise; this could be any data you wish to be processed
further depending on the outcome of the task. More on this later.
Ok, at this point Promises may just seem like a pointless exercise to set some object's state to indicate the status of a task. But as you'll soon see, with this mechanism comes the ability to easily and intuitively define what happens next once the task is completed.
The then() and catch() methods
Whenever you instantiate a Promise object, two methods-
then()
and catch()
- become available to decide what happens
next after the conclusion of an asynchronous task. Take a look at the below:
getImage('doggy.jpg').then(function(successurl){ document.getElementById('doggyplayground').innerHTML = '<img src="' + successurl + '" />' })
Here as soon as "doggy.jpg" has loaded, we specify that the
image be shown inside the "doggyplayground
" DIV. The original
getImage()
function returns a Promise object, so we can call then()
on it to specify what happens when the request has been resolved. The URL
of the image we passed into the resolve()
function when we created
getImage()
becomes available as the parameter inside the
then()
function.
What happens if the image failed to load? The then()
method can
accept a 2nd function to deal with the rejected state of the Promise
object:
getImage('doggy.jpg').then( function(successurl){ document.getElementById('doggyplayground').innerHTML = '<img src="' + successurl + '" />' }, function(errorurl){ console.log('Error loading ' + errorurl) } )
In such a construct, if the image loads, the first function
inside then()
is run, if it fails, the 2nd instead. We can also handle errors
using the catch()
method instead:
getImage('doggy.jpg').then(function(successurl){ document.getElementById('doggyplayground').innerHTML = '<img src="' + successurl + '" />' }).catch(function(errorurl){ console.log('Error loading ' + errorurl) })
Calling catch()
is equivalent to calling then(undefined,
function)
, so the above is the same as:
getImage('doggy.jpg').then(function(successurl){ document.getElementById('doggyplayground').innerHTML = '<img src="' + successurl + '" />' }).then(undefined, function(errorurl){ console.log('Error loading ' + errorurl) })
Using recursion to load and display images sequentially
Lets say we have an array of images we want to load and display
sequentially- that is to say, first load and show image1, and once that's
complete, go on to image2, and so on. We'll talk about chaining promises
together further below to accomplish this, but one approach is
just to use recursion to go through the list of images, calling our getImage()
function each time with a then()
method that shows the current image before
calling getImage()
again until all of the images have been processed. Here is
the code:
var doggyplayground = document.getElementById('doggyplayground') var doggies = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'] function displayimages(images){ var targetimage = images.shift() // process doggies images one at a time if (targetimage){ // if not end of array getImage(targetimage).then(function(url){ // load image then... var dog = document.createElement('img') dog.setAttribute('src', url) doggyplayground.appendChild(dog) // add image to DIV displayimages(images) // recursion- call displayimages() again to process next image/doggy }).catch(function(url){ // handle an image not loading console.log('Error loading ' + url) displayimages(images) // recursion- call displayimages() again to process next image/doggy }) } } displayimages(doggies)
Demo (fetch and display images sequentially):
The displayimages()
function takes an array of images and
sequentially goes through each image, by calling
images.shift()
. For each image, we first call getImage()
to fetch the image,
then the returned Promise object's then()
method to specify what happens next,
in this case, add the image to the doggyplayground
DIV before calling
displayimages()
again. In the case of an image failing to load, the
catch()
method handles those instances. The recursion stops when the doggies array is
empty, after Array.shift()
has gone through all of its elements.
Using recursion with JavaScript Promises is one way to sequentially process a series of asynchronous tasks. Another more versatile method is by learning the art of chaining promises. Lets see what that's all about now.
Chaining Promises
We already know that the then()
method can be invoked on a
Promise instance to specify what happens after the completion of a task.
However, we can in fact chain multiple then()
methods together, in turn chaining
multiple promises together, to specify what happens after each promise has been
resolved, in sequence. Using our trusted getImage()
function to illustrate, the
following fetches one image before fetching another:
getImage('dog1.png').then(function(url){ console.log(url + ' fetched!') return getImage('dog2.png') }).then(function(url){ console.log(url + ' fetched!') }) //Console log: // dog1.png fetched // dog2.png fetched!
So what's going on here? Notice inside the first then()
method,
the line:
return getImage('dog2.png')
This fetches "dog2.png" and returns a Promise object. By
returning a Promise object inside then()
, the next then()
waits for that promise
to resolve before running, accepting as its parameter the data passed on by the
new Promise object. This is the key to chaining multiple promises together- by
returning another promise inside the then()
method.
Note that we can also simply return a static value inside
then()
, which would simply be carried on and executed immediately by the next
then()
method as its parameter value.
With the above example we still want to account for an image not loading, so we'll include the
catch()
method as well:
getImage('baddog1.png').then(function(url){ console.log(url + ' fetched!') }).catch(function(url){ console.log(url + ' failed to load!') }).then(function(){ return getImage('dog2.png') }).then(function(url){ console.log(url + ' fetched!') }).catch(function(url){ console.log(url + ' failed to load!') }) //Console log: // baddog1.png failed to load! // dog2.png fetched!
Recall that catch()
is synonymous with then(undefined,
functionref)
, so after catch()
the next then()
will still be executed.
Notice the organization of the then()
and catch()
methods- we put the return of
the next promise object (or link in the chain) inside its own then()
method,
after the outcome of the previous promise is completely accounted for via the
then()
and catch()
method proceeding it.
If you wanted to load 3 images in succession, for example, we
could just add another set of then()
then()
catch()
to the above code.
Creating a sequence of Promises
Ok, so we know the basic idea of chaining promises together is
to return another promise inside the then()
method. But manually chaining
promises together can quickly become unmanageable. For longer chains, what we
need is a way to start with an empty Promise object and programmatically pile on
the desired then()
and catch()
methods to form the final sequence of promises.
In JavaScript Promises, we can create a blank Promise object that's resolved
to begin with with the line:
var resolvedPromise = Promise.resolve()
There is also Promise.reject()
to create a blank
Promise object that's already in the rejected state. So why would we want a new
Promise object that's already resolved you may ask? Well, it makes for a perfect
Promise object to chain additional promises together, since an already resolved
Promise object will automatically jump to the first then()
method added to it,
and kick start the chain of events.
We can use a resolved Promise object to create a sequence of
promises, by piling on then()
and catch()
methods to it. For example:
var sequence = Promise.resolve() var doggies = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'] doggies.forEach(function(targetimage){ sequence = sequence.then(function(){ return getImage(targetimage) }).then(function(url){ console.log(url + ' fetched!') }).catch(function(err){ console.log(err + ' failed to load!') }) }) //Console log: // dog1.png fetched // dog2.png fetched! // dog3.png fetched! // dog4.png fetched! // dog5.png fetched!
First we create a resolved Promise object called sequence
,
then go through each element inside the doggies[]
array with forEach()
,
adding to sequence
the required then()
and catch()
methods to
handle each image after it's loaded. The result is a series of then()
and
catch()
methods attached to sequence
, creating the desired timeline of loading
each image one at a time.
In case you're wondering, instead of using forEach()
to cycle
through the image array, you can also use a simple for
loop instead, though the
result may be more than you had bargained for:
var sequence = Promise.resolve() var doggies = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'] for (var i=0; i<doggies.length; i++){ (function(){ // define closure to capture i at each step of loop var capturedindex = i sequence = sequence.then(function(){ return getImage(doggies[capturedindex]) }).then(function(url){ console.log(url + ' fetched!') }).catch(function(err){ console.log('Error loading ' + err) }) }()) // invoke closure function immediately } //Console log: // dog1.png fetched // dog2.png fetched! // dog3.png fetched! // dog4.png fetched! // dog5.png fetched!
Inside the for
loop, to properly get the value of
i
at each step
and pass it into then()
, we need to create an outer closure to capture each
value of i
. Without the outer closure, the value of i
passed into
then()
each
time will simply be the value of i
when it's reached the end of the loop, or
doggies.length-1
. If all of this confounds you, the article
JavaScript
closures in for loops should help clear the air.
Creating an array of promises
Instead of chaining promises together, we can also create an
array of promises. This makes it easy to do something after all of the
asynchronous tasks have completed, instead of after each task. For example, the
following uses getImage()
to fetch two images and store them as an array of
promises:
var twodoggypromises = [getImage('dog1.png'), getImage('dog2.png')]
Since getImage()
when called returns a promise,
twodoggypromises
now
contains two Promise objects. So what can we do with an array of promises? Well,
we can then use the static method Promise.all()
to do something after all
promises inside the array have resolved:
Promise.all(twodoggypromises).then(function(urls){ console.log(urls) // logs ['dog1.png', 'dog2.png'] }).catch(function(urls){ // if any image fails to load, then() is skipped and catch is called console.log(urls) // returns array of images that failed to load })
Promise.all()
takes an
iterable (array or array-like list) of promise objects, and waits until all
of those promises have been fulfilled before moving on to any then()
method attached to it. The then()
method is passed an array of returned
values from each promise.
So what happens if one of the promises inside the array doesn't
resolve (is rejected)? In that case the entire then()
portion is ignore, and
is executed instead. So in the above scenario, if one or more of the
images fails to load, it only logs an array of images that failed to load inside
catch()
.
Displaying images when they have all been fetched
It's high time now to see an example of showing off all the
doggies when they have been fetched, instead of one at a time. We'll use
Array.map()
to ease the pain in creating a promise array:
var doggies = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'] var doggypromises = doggies.map(getImage) // call getImage on each array element and return array of promises Promise.all(doggypromises).then(function(urls){ for (var i=0; i<urls.length; i++){ var dog = document.createElement('img') dog.setAttribute('src', urls[i]) doggyplayground.appendChild(dog) } }).catch(function(urls){ console.log("Error fetching some images: " + urls) })
Array.map()
iterates through the original array and calls getImage()
on each element, returning a new array using the return value of getImage()
at
each step, or a promise. The result is twofold- each image gets fetched, and in
turn we get back an array of corresponding promises. Then, we put Promise.all()
to work, passing in doggypromises
to show all the images at once:
Demo (fetch and display images at all once):
Fetch images all at once, but display then in sequence as each one becomes ready
Finally, as if the dogs haven't been paraded enough, lets introduce them to the doggy park in an optimized manner, not one by one, not all at once, but the best of both words. We'll fetch all of the images at once to take advantage of parallel downloading in browsers, but show them in sequence as each one becomes available (fetched). This minimizes the time the doggies show up while still showing them in orderly sequence.
To do this, we just have to do two things we already know- create an array of promises to fetch all images at once (in parallel), then create a sequence of promises to actually show each image one at a time:
var doggies = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'] var doggypromises = doggies.map(getImage) // call getImage on each array element and return array of promises var sequence = Promise.resolve() doggypromises.forEach(function(curPromise){ // create sequence of promises to act on each one in succession sequence = sequence.then(function(){ return curPromise }).then(function(url){ var dog = document.createElement('img') dog.setAttribute('src', url) doggyplayground.appendChild(dog) }).catch(function(err){ console.log(err + ' failed to load!') }) })
Demo (fetch images all at once in parallel, but show them sequentially):
Note that to create our sequence of promises this time, we
iterate through the array of promises generated by Array.map()
, and
not the images array directly. This allows us to create the chain of promises
without having to call getImage()
each time again, which was done
when we decided to fetch all images at once using Array.map()
already.
In conclusion
JavaScript Promises offers an additional way to handle asynchronous tasks at a time where such tasks are becoming intimately woven into the fabric of any modern web site. Used in conjunction with a Polyfill, JavaScript Promises can be put to work today to make the whole affair more intuitive and manageable. Hopefully this tutorial has opened the doors to showing you how just to do that. Check out the following additional resources for more helpful info on the new feature: