Handling async operations gracefully with Promises

Draft
This page is not complete.

Promises are a comparitively new feature of the JavaScript language that allow you to defer further actions until after the previous action has completed, or respond to its failure. This is really useful for setting up a sequence of operations to work correctly. This article shows you how promises work, how you'll see them in use in WebAPIs, and how to write your own.

Prerequisites: Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective: To understand promises and how to use them.

What are promises?

We looked at Promises briefly in the first article of the course, but here we'll look at them in a lot more depth.

Essentially, a Promise is an object that represents an intermediate state of an operation — in effect, a promise that a result of some kind will be returned at some point in the future. There is no guarantee of exactly when the operation will complete and the result will be returned, but there is a guarantee that you'll be able to set up code to run only when the operation completes, either to do something else with a successful result, or to gracefully handle a failure case.

Generally you are less interested in the amount of time an async operation will take to return its result (unless of course it takes far too long!), and more interested in being able to respond to it being returned, whenever that is. and of course, it's nice that it doesn't block the rest of the code execution.

The trouble with callbacks

Let's talk about ordering pizza as an analogy. There are certain steps that you have to take for your order to be successful, which don't really make sense to try to execute out of order, or in order but before each previous step has quite finished:

  1. You choose what pizza you want. This can take a while if you are indecisive, and may fail if you just can't make up your mind, or decide to get a curry instead.
  2. You then place your order and pay for it. This can take a while to return a pizza, and may fail if the restaurant does not have the required ingredients to cook it.
  3. You then collect your pizza and eat. This might fail if ... your car breaks down!

With old-style synchronous callbacks, a pseudo-code representation of this functionality might look something like this:

choosePizza(function(order) {
  placeOrder(order, function(pizza) {
    collectOrder(pizza, function() {
      console.log('Eating pizza, yay!!');
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

This is messy and hard to read (often referred to as "callback hell"), blocks the main thread until it completes, requires you to call the failureCallback() multiple times, once for each nested function, and has other issues besides.

Improvements with promises

Promises make situations like the above much easier to write, parse, and run. If we represented the above pseudo-code using async promises instead, we'd end up with something like this:

choosePizza()
.then(function(order) {
  return placeOrder(order);
})
.then(function(pizza) {
  return collectOrder(pizza);
})
.then(function() {
  console.log('Eating pizza, yay!!');
})
.catch(failureCallback);

This is much better — it is easier to see what is going on, we only need a single .catch() block to handle all the errors, it doesn't block the main thread, and each operation is guaranteed to wait for previous operations to complete before running.

And using arrow functions, you can simplify the code even further:

choosePizza()
.then(order =>
  return placeOrder(order)
)
.then(pizza =>
  return collectOrder(pizza)
)
.then(() =>
  console.log('Eating pizza, yay!!')
)
.catch(failureCallback);

Or even this:

choosePizza()
.then(order => placeOrder(order))
.then(pizza => collectOrder(pizza))
.then(() => console.log('Eating pizza, yay!!'))
.catch(failureCallback);

This works because with arrow functions () => x is valid shorthand for () => { return x; }

Note: You can make further improvements with async/await syntax, which we'll dig into in the next article.

At their most basic, promises are similar to event listeners, but with a few differences:

  • A promise can only succeed or fail once. It cannot succeed or fail twice and it cannot switch from success to failure or vice versa once the operation has completed.
  • If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier.

Explaining basic promise syntax: A real example

Promises are important to understand because most modern Web APIs use them for returning values. To use modern web technologies you'll need to use promises. Later on in the chapter we'll look at how to write your own promise, but for now we'll look at some simple examples that you'll encounter in Web APIs.

In the first example, we'll use the fetch() method to fetch an image from the web, the blob() method to transform the fetch response's raw body contents into a Blob object, and then display that blob inside an <img> element. This is very similar to the example we looked at in the first article of the series, but we'll do it a bit differently as we get you building your own promise-based code.

  1. First of all, download our simple HTML template and the sample image file that we'll fetch.

  2. Add a <script> element at the bottom of the HTML <body>.

  3. Inside your <script> element, add the following line:

    let promise = fetch('coffee.jpg');

    This calls the fetch() method, passing it the URL of the image to fetch from the network as a parameter. This can also take an options object as a optional second parameter, but we are just using the simplest version for now. We are storing the promise call inside a variable called promise. As we said before, this is an intermediate state that is initially neither success or failure.

    Note: When a promise has not completed yet (succeeded or failed) we say it is pending.

  4. To respond to the successful completion of the operation whenever that occurs (in this case, when a Response is returned), we chain a .then() block onto the end of the promise call. In promise terms, we say that the promise has been fulfilled. The callback inside this block (referred to as the executor) will run only when the response has been successfully returned, and is passed the response as a parameter.

    We immediately run the blob() method on this response to transform its raw body into a blob object that we can do something with. The result of this is returned like so:

    response => response.blob()

    which is shorthand for

    function(response) {
      return response.blob();
    }

    OK, enough explanation for now. Add the following line below your first line of JavaScript.

    promise.then(response => response.blob())
  5. Because the blob() method also returns a promise, we can handle the Blob object it returns on fulfillment by chaining another .then() block onto the end of the last one, again using the dot syntax (.) to continue the chain. Because we want to do something a bit more complex to the blob than just run a single method on it and return the result, we'll need to wrap the function body in curly braces this time (otherwise it'll throw an error).

    Add the following to the end of your code, making sure it is right below the previous line (the chain won't work if they are separated):

    .then(myBlob => {
    
    })
  6. Now let's fill in the body of the function. Add the following lines inside the curly braces:

    let objectURL = URL.createObjectURL(myBlob);
    let image = document.createElement('img');
    image.src = objectURL;
    document.body.appendChild(image);

    Here we are running the createObjectURL() method, passing it as a parameter the Blob returned when the second promise fulfills. This will return a URL pointing to the object. Then we create an <img> element, set its src attribute to equal the object URL and append it to the DOM, so the image will display on the page!

  7. If you save the page and load it in your browser, you'll see that it already works. But there is something missing — currently there is nothing to explicitly handle errors if one of the promises fails (rejects, in promise-speak). We can add error handling by chaining a .catch() block onto the end of the chain. Add this now:

    .catch(e => {
      console.log('There has been a problem with your fetch operation: ' + e.message);
    });
  8. To see this in action, try misspelling the URL to the image and reloading the page. The error will be reported in the console of your browser's developer tools.

    This doesn't do much more than it would if you just didn't bother including the .catch() block at all, but think about it — this allows us to control error handling exactly how we want. In a real app, your .catch() block could retry fetching the image, or show a default image, or prompt the user to provide a different image URL, or whatever.

Note: You can see our version of the example live (see the source code also).

Running code in response to multiple promises fulfilling

The above example showed us some of the real basics of using promises. Now let's look at some more advanced features. For a start, chaining processes to occur one after the other is all fine, but what if you want to run some code only after a number of promises have all fulfilled?

You can do this with the Promise.all() static method. This takes an array of promises as a parameter, and will fulfill only if all promises in the array fulfill. It looks something like this:

Promise.all([a, b, c]).then(values => {
  ...
});

If they all fulfill, its executor function will be passed an array containing all those results as a parameter. If any of the promises passed to Promise.all() reject, the whole block will reject.

This can be very useful. Imagine that we’re fetching information to dynamically populate a UI feature on our page with content. It probably makes sense to receive all the data and then show the finished feature, rather than displaying partial information.

Let's build another example to show this in action.

  1. Download a fresh copy of our page template, and again put a <script> element just before the closing </body> tag.

  2. Download our source files (coffee.jpg, tea.jpg, and description.txt), or feel free to substitute your own.

  3. In our script we'll first define a function that returns the promises we want to send to Promise.all(). This would be easy if we just wanted to run the Promise.all() block in response to three fetch() operations completing. We could just do something like:

    let a = fetch(url1);
    let b = fetch(url2);
    let c = fetch(url3);
    
    Promise.all([a, b, c]).then(values => {
      ...
    });

    And values would contain three Response objects.

    However, we don't want to do this. Here we want to run the Promise.all() block when we get back usable blobs representing the images, and a usable text string. We can write a function that does this; add the following inside your <script> element:

    function fetchAndDecode(url, type) {
      return fetch(url).then(response => {
        if(type === 'blob') {
          return response.blob();
        } else if(type === 'text') {
          return response.text();
        }
      })
      .catch(e => {
        console.log('There has been a problem with your fetch operation: ' + e.message);
      });
    }

    This looks a bit complex, so let's run through it step by step:

    1. First of all we define the function, passing it a URL and a string representing the type of resource it is fetching.
    2. Inside the function body, we have a similar structure to what we saw in the first example — we call the fetch() function to fetch the resource at the specified URL, then chain it onto another promise that returns the decoded (or "read") response body. This was always the blob() method in the previous example.
    3. However, two things are different here:
      • First of all, the second promise we chain on is different depending on what the type value is. Inside the executor function we include a simple if ... else if structure to return a different promise depending on what type of file we need to decode (in this case we've got a choice of blob or text, but it would be easy to extend this to deal with other types as well).
      • Second, we don't just run this promise chain in isolation; we have added the return keyword before the fetch() call. The effect this has is to run the entire chain and then run the final result (i.e. the promise returned by blob() or text()) as the return value of the function we've just defined. In effect, the return statements pass the results back up the chain to the top.

    The code inside the function body is async and promise-based, therefore in effect the entire function acts like a promise — convenient.

  4. Next, we run our function three times to fetch and decode the images and text, and store the returned promises in three variables. Add the following below your previous code:

    let coffee = fetchAndDecode('coffee.jpg', 'blob');
    let tea = fetchAndDecode('tea.jpg', 'blob');
    let description = fetchAndDecode('description.txt', 'text');
  5. Next, we will define a Promise.all() block to run some code only when all three of the promises stored above have successfully fulfilled. To begin with, add a block with an empty executor inside the .then() call, like so:

    Promise.all([coffee, tea, description]).then(values => {
    
    });

    You can see that it takes an array containing the promises as a parameter. The executor will only run when all three promises resolve; when that happens, it will be passed an array containing the results from the individual promises (i.e. the decoded response bodies).

  6. Last of all, add the following inside the executor. Here we use some fairly simple sync code to store the results in separate variables (creating object URLs from the blobs), then display the images and text on the page.

    console.log(values);
    // Store each value returned from the promises in separate variables; create object URLs from the blobs
    let objectURL1 = URL.createObjectURL(values[0]);
    let objectURL2 = URL.createObjectURL(values[1]);
    let descText = values[2];
    
    // Display the images in <img> elements
    let image1 = document.createElement('img');
    let image2 = document.createElement('img');
    image1.src = objectURL1;
    image2.src = objectURL2;
    document.body.appendChild(image1);
    document.body.appendChild(image2);
    
    // Display the text in a paragraph
    let para = document.createElement('p');
    para.textContent = descText;
    document.body.appendChild(para);
  7. Save and refresh and you should see your UI components all loaded, albeit in a not particularly attractive way!

The code we provided here for displaying the items is fairly rudimentary, but works as an explainer for now.

Note: If you get stuck, you can compare your version of the code to ours, to see what it is meant to look like — see it live, and see the source code.

Note: If you were improving this code, you might want to loop through a list of items to display, fetching and decoding each one, and then loop through the results inside Promise.all(), running a different function to display each one depending on what the type of code was. This would make it work for any number of items, not just three.

In addition, you could determine what the type of file being fetched is without needing an explicit type property. You could for example check the Content-Type HTTP header of the response in each case using response.headers.get("content-type"), and then react accordingly.

Running some final code after a promise fulfills/rejects

There will be cases where you want to run a final block of code after a promise completes, regardless of whether it fulfilled or rejected. Previously you'd have to include the same code in both the .then() and .catch() callbacks, for example:

myPromise
.then(response => {
  doSomething(response);
  runFinalCode();
})
.catch(e => {
  returnError(e);
  runFinalCode();
});

In more recent modern browsers, the .finally() method was introduced, which can be chained onto the end of your regular promise chain allowing you to cut down on code repetition and do things more elegantly. The above code can now be written as follows:

myPromise
.then(response => {
  doSomething(response);
})
.catch(e => {
  returnError(e);
})
.finally(() => {
  runFinalCode();
});

For a real example, we'd like you to check out our promise-finally.html demo (see the source code also). This works exactly the same as the Promise.all() demo we looked at in the above section, except that in the fetchAndDecode() function we chain a finally() call on to the end of the chain:

function fetchAndDecode(url, type) {
  return fetch(url).then(response => {
    if(type === 'blob') {
      return response.blob();
    } else if(type === 'text') {
      return response.text();
    }
  })
  .catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message);
  }).finally(() => {
    console.log(`fetch attempt for "${url}" finished.`);
  });
}

This logs a simple message to the console to tell us when each fetch attempt has finished.

Building your own promises

It is possible to build your own promises using the Promise() constructor. The main situation in which you'll want to do this is when you've got a sync piece of code that you want to cause to run in an async way, maybe one that uses an older WebAPI that is not promise-based. You can do this by wrapping it in a promise.

The Promise() constructor takes as its parameter an executor function that is passed two parameters, resolve and reject. These represent callbacks that are called when the promise fulfills or rejects, respectively.

Resolving a custom promise

Let's have a look at a simple example to get you started — here we wrap a setTimeout() call with a promise — this runs a function after two seconds that resolves the promise (using the passed resolve() call) with a string of "Success!".

let timeoutPromise = new Promise((resolve, reject) => {
  setTimeout(function(){
    resolve('Success!');
  }, 2000);
});

So when you call this promise and it resolves, you can chain a .then() block onto the end of it and it's executor will be passed a string of "Success!". In the below code we simply alert that message:

timeoutPromise
.then((message) => {
   alert(message);
})

Try running this live to see the result (also see the source code).

The above example is not very flexible — the promise can only ever fulfill with a single string, and it doesn't have any kind of reject() condition specified (admittedly, setTimeout() doesn't really have a fail condition, so it doesn't matter for this simple example).

Rejecting a custom promise

We can create a promise that rejects using the reject() method — just like resolve(), this takes a single value, but in this case it is the value to reject with, i.e., the error available in your .catch() block.

Let's extend the previous example to have a reject() condition as well as allowing different messages to be passed upon success. Remember in the Promise.all() example where we showed how you can have an entire function behave like a single promise? Well, we're going to do something similar here.

Take a copy of the previous example, and replace the existing timeoutPromise definition with this:

function timeoutPromise(message) {
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      resolve(message);
    }, 4000);

    setTimeout(function(){
      reject('Ooops, we have an error');
    }, 3000);
  });
};

So here we have two setTimeout() calls — one that resolves the promise with the message passed into the function after four seconds, and one that rejects the promise with a specific error message after three seconds.

You'll also notice that we've put the return keyword in front of the new Promise() call — this means that when we invoke the timeoutPromise() function, it creates a new promise, onto which you can then chain .then(), .catch(), etc. as standard. Let's use it now — replace the previous timeoutPromise usage with this one:

timeoutPromise('Hello there!')
.then(message => {
   alert(message);
}).
catch(e => {
  console.log(e);
});

When you save and run the code as is, after three seconds you'll get the error message specified inside the reject() call logged to the console, as decreed by the .catch() block. If however you set the first setTimeout() duration to shorter than the second one, you'll see the alert message appear with the message included in it, as decreed by the .then() block.

Note: You can find our version of this example on GitHub as custom-promise2.html (see also the source code).

A more real-world example

The above example was kept deliberately simple to make the concepts easy to understand, but it is not really very useful. The resolve() and reject() conditions are basically faked using setTimeout() calls. To do anything more real-world with custom promises is usually a bit more complicated to achieve.

One example we'd like to invite you to study is Jake Archibald's idb library. This takes the IndexedDB API, which is an old-style callback-based API for storing and retrieving data on the client-side, and allows you to use it with promises. If you look at the main library file you'll see the same kind of techniques we discussed above being used there. The following block promisifys the basic request model used by many IndexedDB methods:

function promisifyRequest(request) {
  return new Promise(function(resolve, reject) {
    request.onsuccess = function() {
      resolve(request.result);
    };

    request.onerror = function() {
      reject(request.error);
    };
  });
}

Conclusion

Promises are a good way to build asynchronous applications when we don’t know the return value of a function or when don’t know how long will it take for the code inside the promise to complete. They will work in the latest version of all modern browsers; the only place where promise support will be a problem is in Opera Mini and IE11 and earlier versions.

We didn't touch on all promise features in this article, just the most interesting and useful ones. As you start to learn more about promises, you'll come across further features and techniques.

Most modern Web APIs are promise-based, so you'll need to understand promises to get the most out of them.

See also

In this module

  • Introducing Async JavaScript
  • Async loops and intervals
  • Handling async operations gracefully with Promises
  • Easier async code with async and await

Document Tags and Contributors

Contributors to this page: chrisdavidmills
Last updated by: chrisdavidmills,