loading...

6 points you need to know about async/await in JavaScript

yashints profile image Yaser Adel Mehraban Originally published at yashints.dev ・5 min read

If you have faced a code like below, then this article will help you in multiple ways 😁.

fetchPizzas()
  .then((pizzas) = {
    return sortByToppings(pizzas)
      .then((pizzas) = {
        return checkDeliveryOptions(pizzas)
          .then((pizzasWithDelivery) = {
            return checkBirthdayGift(pizzasWithDelivery)
              .then((pizza) = {
                return sendToCustomer(pizza);
              });
          });
      });
  });
Enter fullscreen mode Exit fullscreen mode

A little bit of background

There are many a times where we have a bunch of tasks to be executed sequentially. The examples are from File handling to calling databases multiple times based on the result of the previous call. Or calling multiple APIs in a sequence where one call is dependent on another.

Prior to introduction of async/await, many used callbacks alongside setTimeOut to simulated the behaviour they wanted (aka callback hell). Later on people started to use promises which made the code much more readable but they would end up in the same place when the number of calls where high (aka promise hell).

Async functions

A function in JavaScript is async when it operates asynchronously via the event loop, using an implicit promise to return its result. Furthermore, the type of its result should be an AsyncFunction object.

This function is nothing but a combination of promises and generators. I will not going into details of generators, but they usually contains one or many yield keywords.

Now lets see the async function in action. Assume we have a function which returns a string:

function hi() {
  return 'Hi from JavaScript';
}

hi(); // 'Hi from JavaScript'
Enter fullscreen mode Exit fullscreen mode

If we put async in front of the function, then it no longer returns string, it will be a promise which is wrapped around the string value automatically.

async function hi() {
  return 'Hi from JavaScript';
}

hi(); // Promise {<resolved>: "Hi from JavaScript"}
Enter fullscreen mode Exit fullscreen mode

Now in order to get the value from the promise we act like before:

hi().then(console.log); // 'Hi from JavaScript'
Enter fullscreen mode Exit fullscreen mode

You might be wondering how this can help to solve the promise hell. Just bare with me and we'll get there step by step with examples so it'd be clear when we're finished.

Await

The await makes JavaScript engine to wait until a promise is resolved/rejected and returns it's result. This keyword can only be used inside an async function.

const doSomething = async () => {
  console.log(await hi())
};

// 'Hi from JavaScript'
Enter fullscreen mode Exit fullscreen mode

You might think since await forces the JavaScript engine to wait, it will have some cost on CPU. But that's not the case because the engine can perform other scripts while waiting for the promise to get resolves/rejected. Plus this is way more elegant that using promises and .then.

Warning: If you try to invoke an async function using await inside a normal function, you will get a syntax error.

function doSomething() {
  await hi(); // Uncaught SyntaxError: await is only valid in async function
}
Enter fullscreen mode Exit fullscreen mode

A small catch

Most people who start working with async/await forget that they can't invoke an async function on top level code. This is due to the fact that we can't have await inside a normal function and the top level functions are normal by default.

let response = await hi(); // syntax error in top-level code
console.log(response);
Enter fullscreen mode Exit fullscreen mode

What you can do however, is to wrap your code in an async IIFE (immediately invoked function execution) and call it right there:

(async () => {
  let response = await hi(); 
  console.log(response); // 'Hi from JavaScript'
  ...
})();
Enter fullscreen mode Exit fullscreen mode

Update: As Nick Tyler mentioned in the comments, there is a stage 3 proposal to support await in top level code. So stay tuned and watch this space:

GitHub logo tc39 / proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 3)

ECMAScript proposal: Top-level await

Champion: Myles Borins

Status: Stage 3

Synopsis

Top-level await enables modules to act as big async functions: With top-level await, ECMAScript Modules (ESM) can await resources, causing other modules who import them to wait before they start evaluating their body.

Motivation

Limitations on IIAFEs

With await only available within async functions, a module can include an await in the code that executes at startup by factoring that code into an async function:

// awaiting.mjs
import { process } from "./some-module.mjs";
let output;
async function main() {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  output = process(dynamic.default, data);
}
main();
export { output };
Enter fullscreen mode Exit fullscreen mode

This pattern can also be immediately invoked. You could call this an Immediately Invoked Async Function…

Error handling

As I said before, most async functions can be written as a normal function with promises. However, async functions are less error-prone when it comes to error handling. If an awaited call fails, the exception is automatically caught and the Error object will be propagated to the caller using the implicit return promise.

Prior to this, we had to reject the promise which was returned from the normal function and use a .catch in the caller. I've seen many places where the developers used a try/catch and throw a new exception which meant the stack trace would be reset.

async function hi() {
  throw new Error("Whoops!");
};

async function doSomething() {

  try {
    let response = await hi();
    return response;
  } catch(err) {    
    console.log(err);
  }
}

doSomething();
Enter fullscreen mode Exit fullscreen mode

Or you can avoid the try/catch because the promise generated by the call to hi becomes rejected. Then simply use .catch to handle the error.

async function hi() {
  throw new Error("Whoops!");
};

async function doSomething() {
  let response = await hi();
  return response;
}

doSomething().catch(err => {
  console.log(err);
});
Enter fullscreen mode Exit fullscreen mode

You can ignore the catch all together and handle all the exceptions using a global exception handler if you think that's more suitable to your situation. Something like this which uses the onrejectionhandled property of WindowsEventHandlers mixin.

window.onrejectionhandled = function(e) {
  console.log(e.reason);
}
Enter fullscreen mode Exit fullscreen mode

Promise.all compatibility

You can use async/await alongside Promise.all to wait for multiple promises:

const responses = await Promise.all([
  fetch('yashints.dev/rss'),
  hi(),
  ...
])
Enter fullscreen mode Exit fullscreen mode

If an error occurs, it propagates as usual, from the failed promise to Promise.all and then turns to an exception that you can catch using any of the above methods.

await can take in a "thenable"

Similar to promise.then, if you have any object which has a .then method, await will accepts it. This is to support scenarios where a 3rd-party object which is not a promise, but promise-compatible (it supports .then), it would be enough to use it with await.

class Greeting {
  constructor(name) {
    this.name = name;
  }

  then(resolve, reject) {
    console.log(resolve);

    setTimeout(() => resolve(`Hi ${this.name}`));
  }
};

async function greet() {
  const greeting = await Greeting('Yaser');

  console.log(greeting); // Hi Yaser
};

greet();
Enter fullscreen mode Exit fullscreen mode

async class methods

You can have an async class method. Just prepend it with async and you're good to go.

class Order {
  async deliver() {
    return await Promise.resolve('Pizza');
  }
}

new Order()
  .deliver()
  .then(console.log); // Pizza
Enter fullscreen mode Exit fullscreen mode

Summary

Just to quickly go through what we discussed so far:

  1. async keyword makes a method asynchronous, which in turn always returns a promise and allows await to be used.
  2. await keyword before a promise makes JavaScript wait until that is resolved/rejected. If the promise is rejected, an exception is generated, otherwise the result is returned.
  3. Together, they provide a great opportunity for us to write clean, more testable, asynchronous code.
  4. With async/await you wouldn't need .then/.catch, but just note that they are still based on promises.
  5. You can use Promise.all to wait for multiple async functions calls.
  6. You can have an async method in a class.

I know there are many great articles around async/await, but I tried to cover some items where I had to constantly remind myself of. Hope it will help you to have a centralised place for most of what you need to write clean asynchronous JavaScript.

Have fun exploring these points.

Discussion

pic
Editor guide
Collapse
danielsan profile image
Daniel Santana

I’m not sure why the first block of code in the article uses promises like the old callback hell, but here it is the proper way to write that first block of code:

fetchPizzas()
  .then( sortByToppings )
  .then( checkDeliveryOptions )
  .then( checkBirthdayGift )
  .then( sendToCustomer ); 

And, in my opinion, it looks better then the alternatives offered by the article, not mentioning the testability of it.

Collapse
avalander profile image
Avalander

Agree, I think promises are better that async/await when it comes to function composition.

Collapse
yashints profile image
Yaser Adel Mehraban Author

That was a bad example, not to be used

Collapse
nickytonline profile image
Nick Taylor (he/him)

Thanks for the post Yaser!

One thing I'd add is that although we don't have top-level await, it's most likely going to happen as it's at Stage 3.

GitHub logo tc39 / proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 3)

ECMAScript proposal: Top-level await

Champion: Myles Borins

Status: Stage 3

Synopsis

Top-level await enables modules to act as big async functions: With top-level await, ECMAScript Modules (ESM) can await resources, causing other modules who import them to wait before they start evaluating their body.

Motivation

Limitations on IIAFEs

With await only available within async functions, a module can include an await in the code that executes at startup by factoring that code into an async function:

// awaiting.mjs
import { process } from "./some-module.mjs"
let output
async function main() {
  const dynamic = await import(computedModuleSpecifier)
  const data = await fetch(url);
  output = process(dynamic.default, data);
}
main();
export { output };

This pattern can also be immediately invoked. You could call this an Immediately Invoked Async Function Expression (IIAFE), as a play on IIFE idiom.

// awaiting.mjs
import
Collapse
yashints profile image
Yaser Adel Mehraban Author

Thanks for sharing, didn't know about this 👌🏽

Collapse
yashints profile image
Yaser Adel Mehraban Author

Updated the post to reflect this, thanks again Nick

Collapse
lostdesign profile image
André

Great article Yaser! btw: AsyncFuncton -> AsyncFunction

Collapse
yashints profile image
Yaser Adel Mehraban Author

Nice catch, I should double check my spell checker in VS Code

Collapse
ckromero profile image
Christopher Romero

Good read, but I believe next to last code line should be ‘deliver()’. Also, looking for more on thenable, any good coverage somewhere? Thanks!!

Collapse
yashints profile image
Collapse
nokiz profile image
Nando AM

Just what I needed when I needed. Thank you!!

Collapse
yashints profile image
Yaser Adel Mehraban Author

You're welcome, glad it helped