JavaScript 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:

Javascript async
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:

Javascript async function returned a promise
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:

Javascript async function 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:

Javascript async/await thenable
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:

Javascript async/await
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:

Javascript async/await try..catch
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:

Javascript async/await try..catch
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:

Javascript async/await
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.

Use async/await to wait for a variable to be defined before continuing execution

It is possible to use async/await to wait for a variable to be defined before continuing execution in JavaScript. Here's an example:

Use async/await to wait for a variable to be defined before continuing execution
function waitForVariable() { return new Promise((resolve) => { const intervalId = setInterval(() => { if (typeof myVariable !== "undefined") { clearInterval(intervalId); resolve(); } }, 100); }); } async function myFunction() { await waitForVariable(); // Execution will continue here once myVariable is defined console.log(myVariable); } let myVariable; // variable is undefined at this point setTimeout(() => { myVariable = "Hello, world!"; // define myVariable after a delay }, 2000); myFunction(); // call myFunction

In this example, waitForVariable() returns a Promise that resolves when the myVariable variable is defined. The myFunction() function uses await to wait for this Promise to resolve before continuing execution. Once myVariable is defined, the function logs its value to the console.

Note that in this example, we're using a setInterval function to periodically check whether myVariable has been defined yet. You could also use other methods to detect when the variable is defined, such as attaching an event listener or using a callback function. The key is to wrap the wait for the variable in a Promise that resolves when the variable is defined, and then use await to wait for the Promise to resolve before continuing execution.

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.

Practice Your Knowledge

What is the function of the 'async' keyword in JavaScript?

Quiz Time: Test Your Skills!

Ready to challenge what you've learned? Dive into our interactive quizzes for a deeper understanding and a fun way to reinforce your knowledge.

Do you find this helpful?