async and await

Write asynchronous code that reads top to bottom, catch its errors with try and catch, and run independent work in parallel.

  • Beginner
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Rewrite promise chains with await
  • Run independent tasks in parallel
  • Handle async errors with try, catch and finally
  • Avoid await mistakes in loops

Before you start

Promise chains are flatter than nested callbacks, but they still split your logic into many small handlers. async and await let you write the same promise-based code as if it were ordinary, step-by-step code, with normal variables, if statements, loops and try...catch. Under the hood, everything is still promises.

async functions

Put async in front of a function and two things change:

  1. The function always returns a promise. A returned value becomes the fulfilled value; a thrown error becomes the rejection.
  2. You can use await inside it.
JavaScript
async function getAnswer() {
  return 42;
}

console.log(getAnswer());                // → Promise { 42 } (in Node.js; browsers show Promise {<fulfilled>: 42})
getAnswer().then((n) => console.log(n)); // → 42

Every function form can be async:

JavaScript
async function load() {}
const loadAll = async () => {};
const api = {
  async save() {},
};

await

await takes a promise and pauses the async function until that promise settles. If it fulfils, await gives you the value. If it rejects, await throws the reason as an error.

Here are the helpers from the previous lesson again, so the examples run on their own:

JavaScript
const courses = [
  { id: 1, title: "JavaScript Basics", authorId: 7 },
  { id: 2, title: "CSS Grid in Practice", authorId: 9 },
];

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function loadCourse(id) {
  await delay(500);
  const course = courses.find((c) => c.id === id);
  if (!course) throw new Error(`No course with id ${id}`);
  return course;
}

Notice that loadCourse no longer needs new Promise. Being async already makes it return a promise, and throw already makes that promise reject.

Compare a chain with its await version:

JavaScript
// Promise chain
function showCourseWithThen(id) {
  return loadCourse(id)
    .then((course) => loadAuthor(course.authorId))
    .then((author) => render(author)); // course is out of scope here
}

// async/await
async function showCourse(id) {
  const course = await loadCourse(id);
  const author = await loadAuthor(course.authorId);
  render(course, author);
}

In the await version, course is still in scope when render needs it. In the chain, it was trapped inside the first handler. That alone removes most reasons for nesting.

await pauses the function, not the page

await does not block the thread. When an async function reaches await, it gives control back to whoever called it. Other code, clicks and timers carry on. The function resumes later as a microtask, once the promise settles.

JavaScript
async function task() {
  console.log("2: task starts");
  await null;
  console.log("4: task resumes");
}

console.log("1: before");
task();
console.log("3: after");

// → 1: before
// → 2: task starts
// → 3: after
// → 4: task resumes

The body runs synchronously up to the first await. Even await null, which waits for a value that is not a promise at all, hands control back and resumes as a microtask, after the current synchronous code has finished.

Errors: try, catch and finally

Because a rejected await throws, ordinary try...catch works again:

JavaScript
async function showCourse(id) {
  try {
    const course = await loadCourse(id);
    console.log(course.title);
  } catch (error) {
    console.error("Could not load course:", error.message);
  } finally {
    console.log("Done");
  }
}

await showCourse(99);
// → Could not load course: No course with id 99
// → Done

The await showCourse(99) line uses top-level await: await outside any function. It works in the browser console, in ES modules (lesson 34) and in .mjs files in Node.js. In a classic <script> or a CommonJS file it is a syntax error, so wrap the call in an async function there.

If you do not catch inside the function, the error rejects the promise the function returns, and the caller must handle it:

JavaScript
showCourse(1).catch((error) => console.error(error));

Catch errors where you can do something useful about them, such as showing a message. Code that cannot recover should let the error travel up to a caller that can.

Sequential versus parallel

Each await waits for the previous one to finish. That is right when a step needs the previous result, but wasteful when the steps are independent.

JavaScript
// Sequential: about 1000ms
console.time("sequential");
const first = await loadCourse(1);
const second = await loadCourse(2);
console.timeEnd("sequential"); // → sequential: 1004ms (roughly)

The second request does not start until the first finishes. To run independent tasks at the same time, start them all first and then wait for all of them with Promise.all():

JavaScript
// Parallel: about 500ms
console.time("parallel");
const [a, b] = await Promise.all([loadCourse(1), loadCourse(2)]);
console.timeEnd("parallel"); // → parallel: 502ms (roughly)
console.log(a.title, "and", b.title);
// → JavaScript Basics and CSS Grid in Practice

Promise.all() takes an array of promises and returns one promise that fulfils with an array of their results, in the same order. If any of them rejects, the combined promise rejects with that error straight away. Lesson 43 covers Promise.all() and its siblings, including how to keep the successful results when some tasks fail.

The key idea: a promise starts its work when it is created, not when it is awaited. Calling loadCourse(1) and loadCourse(2) without await starts both timers at once.

await in loops

for…of runs one at a time

JavaScript
for (const id of [1, 2]) {
  const course = await loadCourse(id);
  console.log(course.title);
}

This is sequential, which is what you want when order matters or when a server limits how many requests you may send at once.

map plus Promise.all runs in parallel

JavaScript
const titles = await Promise.all(
  [1, 2].map(async (id) => {
    const course = await loadCourse(id);
    return course.title;
  })
);
console.log(titles); // → [ 'JavaScript Basics', 'CSS Grid in Practice' ]

forEach does not wait

JavaScript
// Bug: "All loaded" logs first
[1, 2].forEach(async (id) => {
  const course = await loadCourse(id);
  console.log(course.title);
});
console.log("All loaded");

forEach calls the async callback, ignores the promise it returns and moves on. Nothing waits, and nothing catches errors. Use for...of or map with Promise.all() instead.

Common mistakes

Forgetting await

JavaScript
async function printTitle() {
  const course = loadCourse(1); // missing await
  console.log(course.title);    // → undefined
}

course is a promise, which has no title property. If a value is unexpectedly undefined or logs as a Promise, look for a missing await.

Returning without await inside try

JavaScript
async function safeLoad(id) {
  try {
    return await loadCourse(id); // await needed for catch to see the error
  } catch {
    return null;
  }
}

Without await, the function returns the pending promise straight away and the try block has already ended when it rejects, so catch never runs. Inside try, write return await.

Mixing styles

You can call .then() on the result of an async function, and await any promise, so the two styles mix freely. Pick one style per function: await for most code, .then() for a quick one-liner.

Summary

  • An async function always returns a promise. return fulfils it and throw rejects it.
  • await pauses only the async function until a promise settles, then gives you the value or throws the error.
  • Use try, catch and finally around awaited calls, or let the caller handle the rejection.
  • Top-level await works in modules and the console, not in classic scripts.
  • Independent work should run in parallel: start the promises, then await Promise.all([...]).
  • for...of with await is sequential; map with Promise.all() is parallel; forEach with an async callback never waits.
  • Inside try, use return await so catch can see the rejection.

Further reading