Skip to main content

Write Asynchronous JavaScript

Learning Objectives

After completing this unit, you'll be able to:

  • Describe a common JavaScript pitfall known as the Pyramid of Doom.
  • Recognize the commonly used structure of a promise.
  • Demonstrate how asynchronous calls can be chained together using promises.
  • Demonstrate how an async function can be used to call a promise.

Avoiding the Pyramid of Doom

JavaScript is single threaded, which means that only one function can run at a time. Therefore, coding in JavaScript inevitably means working with asynchronous code. This is especially true when you need to do anything that involves I/O (input/output). Like getting data from a database, making a call to an API, or even just waiting for input from a user. Because any functions that do these types of things will undoubtedly block the browser.

For a long time, using callback functions was one of the ways you did this in JavaScript. A callback is just a function that executes after another function has finished executing. For example, if you were to run the following code in PlayCode and then look at the Console, you would see the message, "1st Call".

function doSomething(msg, callback){
  setTimeout(
    function () {
      console.log(msg);
      callback();
    },
    1000);
}
doSomething("1st call", function() {});

Of course, it would take 1 second or 1000 milliseconds for that to happen since the doSomething function uses the browsers built-in setTimeout method to simulate a delayed response. This kind of code works fine and most developers can understand it. But what happens if you want to call this function multiple times, but only after the previous call finishes? And what happens if one of those callback functions fails?

Have you ever heard of “the pyramid of doom”? And no, it's not the name of an Indiana Jones movie. The pyramid of doom is typically used to identify asynchronous code that is deeply nested, which tends to result in a pyramid-looking shape.

JavaScript code used to depict the pyramid shape that appears when callback code is deeply nested.

Basically it's code built from several dependent asynchronous functions that can all potentially have errors. Trust us, code like this can get real messy, real quick.

A Promise Is a Promise

If you've been doing JavaScript asynchronous development for a few years, then you may have come across one or more libraries that implemented some kind of promise pattern. For example, the very popular jQuery library introduced a chainable object called the Deferred object, which was able to return a promise object.

A promise is just that: a promise to return something at a later time. Either the thing you wanted is returned, or an error. ES6 introduced promises natively to JavaScript in the form of a Promise object. Promises in ES6 are based on the Promises/A+ open standard and offer many advantages to traditional callback functions. The most important advantage is how easy it is to chain asynchronous functions together.

To see how they work, let's start by rewriting the doSomething function so that it uses ES6 promises.

function doSomething(msg){
  return new Promise(
    function (resolve, reject) {
      setTimeout(
        function () {
          console.log(msg);
          resolve();
        },
        1000);
    });
}
    

doSomething("1st Call")
  .then(function() {
    return doSomething("2nd Call");
  })
  .then(function() {
    return doSomething("3rd Call");
});

The actual doSomething function, now returns a new Promise object. And instead of calling the callback, the function now calls resolve.

The cool thing here is how the doSomething function is called. We can now use the then method to specify what gets called only after the first function completes. Running this code in PlayCode should result in three messages displayed in the Console, "1st Call" followed by "2nd Call" and then "3rd Call". See how easy it was to chain those functions together?

But hold on. Remember the arrow functions from the unit on functions? Well, by combining promises with arrow functions, the code becomes even easier to read.

function doSomething(msg){
  return new Promise((resolve, reject) => {
      setTimeout(
        () => {
          console.log(msg);
          resolve();
        },
        1000);
    })
}
    

doSomething("1st Call")
  .then(() => doSomething("2nd Call"))
  .then(() => doSomething("3rd Call"));

And to see what happens when calling the reject method, we need to add some code in the setTimeout handler that intentionally throws an error.

function doSomething(msg){
  return new Promise((resolve, reject) => {
      setTimeout(
        () => {
          try {
            throw new Error('bad error');
            console.log(msg);
            resolve();
          } catch(e) {
            reject(e);
          }
        },
        1000);
    })
}
    

doSomething("1st Call")
  .then(() => doSomething("2nd Call"))
  .then(() => doSomething("3rd Call"))
  .catch(err => console.error(err.message));

Notice that we added the catch method to the very end of the call and if triggered, the error object is returned. To print out the error message, we need to specify the message property name.

Technically, you could have passed the failure function as a parameter with each then method call. But, calling the error function at the bottom of the chain using the catch method is a best practice since it catches all errors produced from the chain.

Another Way to Await and See

The ES2016+ release introduced async functions and a different way of calling native promises. The structure of the promise remains the same, but what changes is how the promise is called. The call is wrapped in a function that uses the async and await keywords. What is returned is a promise object that contains either the resolved or rejected value. For the doSomething function, the call looks like this:

async function doSomethingManyTimes() {
  try {
    await doSomething("1st Call");
    await doSomething("2nd Call");
    await doSomething("3rd Call");
  } catch (e) {
    console.error(e.message);
  }
}
      

doSomethingManyTimes();

Many developers think this syntax is easier to read and understand. It resembles the top-down approach that you see in traditional synchronous code. But really, it's just a different way of working with promises.

Tell Me More

  • The Promise object includes four methods that you may want to check out for more advanced promise scenarios. They include:
    • Promise.all(iterable)—Returns promise only after all the promises in the iterable have resolved or any are rejected.
      If the returned promise resolves, it is resolved with an aggregating array of the values from the resolved promises, in the same order as defined in the iterable of multiple promises.

      If it rejects, it is rejected with the reason from the first promise in the iterable that was rejected.

    • Promise.race(iterable)—Returns promise after the first promise in the iterable has resolved or rejected.
    • Promise.resolve(value)—Returns promise that is resolved with the value passed in as a parameter.
    • Promise.reject(reason)—Returns a promise that is rejected with the reason passed in as a parameter.
  • If you have ever had to do a JavaScript data request using XMLHttpRequest, then you know it is not a particularly easy thing to do. The Fetch API allows you to do this with much less than code and as a bonus it returns a promise object. It is the perfect companion for native ES6 promises. You can learn more about it from the links in the Resources section.

Resources

無料で学習を続けましょう!
続けるにはアカウントにサインアップしてください。
サインアップすると次のような機能が利用できるようになります。
  • 各自のキャリア目標に合わせてパーソナライズされたおすすめが表示される
  • ハンズオン Challenge やテストでスキルを練習できる
  • 進捗状況を追跡して上司と共有できる
  • メンターやキャリアチャンスと繋がることができる