Async/await

There exists a unique syntax that can enhance your performance with promises. It’s async/await: a surprisingly easy and comfortable means to deal with promises.

Async functions

The async function declaration specifies an asynchronous function, which can return an AsyncFunction object. Async functions perform in a separate order than the rest of the code through the event loop and return a Promise as its result. Yet, the structure and the syntax of the code look similar to standard synchronous functions.

As a rule, the keyword async is placed before a function, like here:

async function fn() {
  return 1;
}

Putting it before a function means that a function always returns a promise.

Let’s check out a case, where the function automatically returns a resolved promise with the result of 1:

async function fn() {
  return 1;
}
fn().then(console.log); // 1

You could explicitly return a promise that would be the same. Here is an example:

async function fn() {
  return Promise.resolve(1);
}
 
fn().then(console.log); // 1

So, let’s assume that async returns a promise, wrapping non-promises in it. It can make your tasks much more manageable. Moreover, there exists another keyword, await, which works inside the async functions.

Await

The await keyword makes JavaScript wait till the promise settles and returns the result.

The syntax looks like this:

//  only works inside async functions
let val = await promise;

Let’s explore an example with a promise resolving in a second:

async function fn() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("Done!"), 1000)
  });
  let res = await promise; // wait until the promise resolves (*)
  console.log(res); // result:  "Done!"
}
fn();

The function execution is suspended at the line (*) and resumed when the promise settles with res transforming into its result. So, in a second, the code shows “done”.

await doesn’t cost any CPU resources, as the engine is capable of doing other jobs in the meantime.

So, await can be considered a more elegant way to get the promise result than promise.then.

Note that you can’t use await in regular functions: a syntax error will occur.

For instance:

function fn() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

The error, in the example above, will occur if you don’t put async before a function. So, await always works inside an async function.

Also, consider that await doesn’t operate in the top-level code.

See the following example:

// syntax error in top-level code
let response = await fetch('/promiseChaining/user.json');
let user = await response.json();

We can assume that the code above will not work. But, you have the option of wrapping it into an anonymous async function, as follows:

(async() => {
  let response = await fetch('/promiseChaining/user.json');
  let user = await response.json();
  ...
})();

await allows using thenable objects, like promise.then.

Here the primary idea is that a third-party object might not be a promise, but promise-compatible, in case it supports .then, it’s enough for using with await.

This is illustrated in the example below:

class Thenable {
  constructor(number) {
    this.number = number;
  }
  then(resolve, reject) {
   console.log(resolve);
    // resolve with this.num+2 after 2000ms
    setTimeout(() => resolve(this.number + 2), 2000); // (*)
  }
};
async function fn() {
  // waits for 2 second, then result becomes 4
  let result = await new Thenable(2);
  console.log(result);
}
fn();

In the event of getting a non-promise object with .then, await calls that method and provides the built-in functions resolve and reject as arguments. Afterward, await waits till one of them is called, then proceeds with the results.

If you want to declare an async class method, you should prepend it with async, as follows:

class  Supplier{
  async wait() {
    return await Promise.resolve(1);
  }
}
new Supplier()
  .wait()
  .then(console.log); // 1

The idea is the same: ensuring that the returned value is a promise and enabling await.

Error Handling

The await promise returns a result when a promise resolves typically. But, in the event of a rejection, it throws the error, just if it were a throw statement at that line.

The following two codes are equivalent:

async function fn() {
  await Promise.reject(new Error("Error!!"));
}

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

In real situations, the promise takes some time before the rejection. In such a case, there might be a delay before await throws an error.

It is possible to catch that error using try..catch like a regular throw.

One example is the following:

async function fn() {
  try {
    let response = await fetch('http://noSuchUrl');
  } catch (err) {
    console.log(err); // TypeError: failed to fetch
  }
}
fn();

If an error occurs, the control jumps to the catch block. You can also wrap multiple lines, like here:

async function fn() {
  try {
    let response = await fetch('/noUserHere');
    let user = await response.json();
  } catch (err) {
    // catches errors both in fetch and response.json
    console.log(err);
  }
}
fn();

In case, you don’t have try..catch, the promise created by the call of the async function fn() is rejected. So, .catch can be appended for handling it:

async function fn() {
  let response = await fetch('http://noSuchUrl');
}

// fn() becomes a rejected promise
fn().catch(console.log); // TypeError: failed to fetch (*)

Another important note: if you forget to add .catch, you will get an unhandled promise error. Such kinds of errors can be handled with unhandledrejection event handler. We have already explored it in the chapter Error handling with promises.

Async/await and promise.then/catch

While using async/await, you may rarely need to apply .then, as await handles the waiting for you. Moreover, it is possible to use try..catch instead of .catch. Usually, it’s handier.

But, as it was already mentioned, at the top level of the code, when you are outside any async function, you will syntactically not be able to use await. Hence, it’s a common practice to add .then/catch for handling the final result or falling-through an error, as in the line (*) of the case above.

Async/await and Promise.all

Async/await, operates efficiently with Promise.all.

For instance, when you need to wait for multiple promises, you can easily wrap then into Promise.all and then await, like here:

// wait for the array of results
let results = await Promise.all([
  fetch(urlOne),
  fetch(urlTwo),
  ...
]);

If an error occurs, it propagates normally: from the failed promise to Promise.all, then becomes an exception, which you can catch implementing try..catch around the call.

Summary

In general, we can distinguish the following two effects of the async keyword:

  1. Making it always return a promise.
  2. Allowing await to be used inside it.

The keyword await that is put before a promise makes JavaScript wait till the promise settles and then acts as follows:

  1. In case an error occurs, the exception is generated: the same as if the throw error.
  2. In other cases, it will return the result.

Together async and await provide an excellent framework for writing asynchronous code, easy to write, and read.

Another prominent advantage: you will rarely need to write promise.then/catch with async/await. Also, Promise.all is there to help you whenever you are waiting for multiple tasks simultaneously.




Do you find this helpful?

Related articles