async and await

async and await let you write code that waits for promises as if it were a recipe, one step under another, with try and catch for the steps that fail.

Journeyman · JavaScript · Updated

After this lesson you can

  • Write an async function that awaits each step in order and catches a failure with try and catch
  • Explain what an async function returns, and spot a missing await

Key takeaways

  • An async function always returns a promise. Whatever it returns becomes the promise's value, and an error it throws rejects the promise.
  • await, read "wait for this", pauses the async function until a promise settles and gives you its value. The rest of the program keeps running.
  • Put await inside try and catch to handle a rejected promise, the same way you handle any other error.
  • Forget await and you get the promise instead of its value, such as [object Promise] in a message, and try and catch can't see its failure.
  • await works inside an async function, and at the top level of a module. Anywhere else it's a SyntaxError.

Open almost any modern JavaScript project, a React app, a Node server, a browser extension, and you'll see the words async and await everywhere. They're how professionals write code that waits: load the user, then load their orders, then show the page, with a friendly message if anything fails. They work on the same promises you met in Promises: then, catch and finally, but they let you write the steps one under another, the way you'd write a recipe, instead of as a chain of .then calls.

An async function always returns a promise

Put the word async in front of a function, and the function always returns a promise, whatever its return says:

async function pizzaOfTheDay() {
  return 'Pepperoni';
}

const result = pizzaOfTheDay();
console.log(result);

result.then((pizza) => console.log(`Today's pizza: ${pizza}`));
Promise { 'Pepperoni' }
Today's pizza: Pepperoni

The function returned the text 'Pepperoni', but the caller got a promise fulfilled with it. In the same way, if an async function throws an error, the caller gets a rejected promise. An arrow function can be async too: const pizzaOfTheDay = async () => 'Pepperoni';.

await: wait for this

Inside an async function, await (read it "wait for this") goes in front of a promise. It pauses the function until the promise settles, then hands you the value, so you can store it in a variable like any other value:

function placeOrder(pizza) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Your ${pizza} is ready`), 1000);
  });
}

async function pickUp() {
  console.log('Waiting for the buzzer...');
  const message = await placeOrder('Hawaiian');
  console.log(message);
}

pickUp();
console.log('Meanwhile, the next customer orders');
Waiting for the buzzer...
Meanwhile, the next customer orders
Your Hawaiian is ready

await pauses only pickUp, not the whole program. While pickUp waits for its pizza, the line after pickUp(); runs, and in a web page, clicks and typing keep working. When the promise is fulfilled, pickUp carries on from the line it stopped at.

Try it. Save it as pickup.js and run node pickup.js. Then add a second await placeOrder('Veggie') inside pickUp and predict how long the whole run takes before you try it.

The whole order, top to bottom

Here's the chain from the last lesson, written with await. Each step waits for the one before it, and its result lands in a variable with a name:

function step(message, ms) {
  return new Promise((resolve) => setTimeout(() => resolve(message), ms));
}

async function makePizza() {
  const dough = await step('Dough stretched', 200);
  console.log(dough);
  const toppings = await step('Sauce and cheese on', 100);
  console.log(toppings);
  const baked = await step('Baked for 90 seconds', 300);
  console.log(baked);
  return 'Boxed and ready';
}

makePizza().then((message) => console.log(message));
Dough stretched
Sauce and cheese on
Baked for 90 seconds
Boxed and ready

It prints the same lines as the chain, but it reads like the recipe card on the kitchen wall. Both ways are correct, and you'll meet both in other people's code: await is how most new code is written, and .then is still handy for one quick step.

try and catch around await

When a promise you await is rejected, await throws its reason as an error, right on that line. So you handle it with try and catch, as in Handling Errors with try, catch and finally: the code that might fail goes in the try { } block, and if anything in it throws, JavaScript jumps to the catch { } block, with the error in a variable. A finally { } block runs either way.

function addTopping(topping) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (topping === 'mushrooms') {
        reject(new Error("Sorry, we're out of mushrooms"));
      } else {
        resolve(`${topping} added`);
      }
    }, 300);
  });
}

async function orderPizza(toppings) {
  try {
    for (const topping of toppings) {
      const message = await addTopping(topping);
      console.log(message);
    }
    console.log('Into the oven!');
  } catch (error) {
    console.log(`Order stopped: ${error.message}`);
  } finally {
    console.log('Ticket cleared');
  }
}

orderPizza(['peppers', 'mushrooms', 'olives']);
peppers added
Order stopped: Sorry, we're out of mushrooms
Ticket cleared

The peppers went on. The mushrooms' promise was rejected, so await threw, and the rest of the try was skipped: no olives, no oven. The catch told the customer, and the finally cleared the ticket.

Try it. Change the list to ['peppers', 'olives']. Which lines print now?

The mistake of forgetting await

Leave out await, and nothing waits: you get the promise itself, the buzzer instead of the pizza.

function placeOrder(pizza) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Your ${pizza} is ready`), 1000);
  });
}

async function checkOrder() {
  const message = placeOrder('Veggie');
  console.log(`Buzzer says: ${message}`);
}

checkOrder();
Buzzer says: [object Promise]

[object Promise] is how a promise looks when it's turned into text. When you see it on a page or in the console, look for a missing await. In the same way, Promise { <pending> } in a console.log means you logged the promise, not its value.

There's a sneakier result too. Without await, a rejected promise isn't thrown inside the try, so catch never sees it:

function addTopping(topping) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (topping === 'mushrooms') {
        reject(new Error("Sorry, we're out of mushrooms"));
      } else {
        resolve(`${topping} added`);
      }
    }, 300);
  });
}

async function orderPizza() {
  try {
    addTopping('mushrooms');
    console.log('Topping ordered');
  } catch (error) {
    console.log(`Order stopped: ${error.message}`);
  }
}

orderPizza();

It prints Topping ordered, which isn't true, and a moment later Node stops the program with Error: Sorry, we're out of mushrooms: an unhandled rejection. The try had already finished by the time the promise was rejected. With await addTopping('mushrooms');, the catch handles it.

One more rule: await only works inside an async function, and at the top level of a module file (see Modules: import and export). Use it inside an ordinary function, and the program won't start: Node reports a SyntaxError, Unexpected reserved word in a module file, or await is only valid in async functions and the top level bodies of modules in a plain script.

Check yourself

2 questions to try on the lesson page.

So what

async and await let you write waiting code the way you think about it: do this, wait for it, then do the next thing, and say sorry when it fails. The next lesson, Waiting for Several Things at Once, makes it fast: instead of cooking one pizza after another, the whole table's order goes into the oven together.

Key terms

Cheat sheets

Related