Complete JavaScript Course Module 12: Advanced: Asynchronous JavaScript

Async and Await in JavaScript

  • Beginner
  • Lesson 12.3
  • 5 min read
  • 54 of 76 in the course

In short

Async and await in JavaScript let you write promise code that reads like normal, step-by-step code. Learn error handling, parallel tasks and common mistakes. What does async do in JavaScript? Putting the word async before a function does two things. First, the function always returns a promise.

Async and await in JavaScript let you write promise code that reads like normal, step-by-step code. Learn error handling, parallel tasks and common mistakes.

What does async do in JavaScript?

Putting the word async before a function does two things. First, the function always returns a promise. If you return a normal value, JavaScript wraps it in a fulfilled promise. Second, you are allowed to use await inside it.

async function getGreeting() {
  return "Hello!";
}

const result = getGreeting();
console.log(result);              // Output: Promise {<fulfilled>: 'Hello!'}
result.then((text) => console.log(text)); // Output: Hello!

Even though the function just returns a string, you get a promise back. Arrow functions can be async too: const getGreeting = async () => "Hello!";.

How to use await

await pauses the async function until a promise settles, then gives you its value. While it waits, the rest of the page keeps running, so nothing freezes. It is like the async waiter from the first lesson: the waiter waits for your food, but serves other tables meanwhile.

Here is the user-and-orders example from the promises lesson, rewritten with await:

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getUser = async (id) => { await wait(500); return { id, name: "Maya" }; };
const getOrders = async (user) => { await wait(500); return [{ id: 101, total: 49.99 }]; };

async function showOrders() {
  const user = await getUser(7);       // wait for the user
  const orders = await getOrders(user); // then wait for the orders
  console.log(`${user.name} has ${orders.length} order(s)`);
}

showOrders();

// Output (after 1 second):
// Maya has 1 order(s)

No .then(), no nesting, and user is available on every line. This is why most modern code uses async/await.

How to handle errors with async/await and try/catch

When an awaited promise is rejected, await throws the error. That means you can use the same try/catch you already know.

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

async function loadProfile(id) {
  await wait(500);
  if (id <= 0) throw new Error("Invalid user id");
  return { id, name: "Maya" };
}

async function start() {
  try {
    console.log("Loading...");
    const profile = await loadProfile(-1);
    console.log("Loaded:", profile.name);
  } catch (error) {
    console.log("Error:", error.message);
  } finally {
    console.log("Done loading");
  }
}

start();

// Output:
// Loading...
// Error: Invalid user id
// Done loading

The throw inside an async function rejects its promise. The await in start turns that rejection back into a thrown error, which catch handles. Change -1 to 5 and you will see Loaded: Maya.

How to run async tasks in parallel vs in sequence

If each await is on its own line, the tasks run in sequence: the second starts only after the first finishes. That is right when step 2 needs the result of step 1. But if the tasks are independent, you waste time.

To run them in parallel, start all the promises first, then wait for them together with Promise.all.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getWeather = async () => { await wait(1000); return "Sunny"; };
const getNews = async () => { await wait(1000); return "3 new stories"; };

async function inSequence() {
  console.time("sequence");
  const weather = await getWeather(); // 1 second
  const news = await getNews();       // another 1 second
  console.timeEnd("sequence");        // Output: sequence: ~2000 ms
}

async function inParallel() {
  console.time("parallel");
  const [weather, news] = await Promise.all([getWeather(), getNews()]);
  console.timeEnd("parallel");        // Output: parallel: ~1000 ms
  console.log(weather, "-", news);    // Output: Sunny - 3 new stories
}

inSequence().then(inParallel);

Promise.all returns an array, and array destructuring (const [weather, news]) puts each value in its own variable. The parallel version is twice as fast.

What is top-level await?

Normally you can only use await inside an async function. But inside a JavaScript module (a script with type="module", covered in the Modules lesson), you can use await at the top level of the file.

<script type="module">
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  await wait(1000);
  console.log("Module ready after 1 second");
</script>

In a normal <script> without type="module", this line would be a syntax error. Modern browser consoles also allow top-level await for quick tests.

Common mistakes with async/await

Common mistake: Forgetting await. If you write const user = getUser(7);, then user is a pending promise, not the user. user.name will be undefined. If a value prints as Promise {<pending>}, you forgot await.

The second big mistake is using await inside forEach. forEach does not wait for async callbacks, so the code after it runs too early.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const ids = [1, 2, 3];

async function wrong() {
  ids.forEach(async (id) => {
    await wait(300);
    console.log("forEach item", id);
  });
  console.log("forEach finished?"); // runs FIRST, before any item
}

async function right() {
  for (const id of ids) {
    await wait(300);
    console.log("for...of item", id);
  }
  console.log("for...of finished"); // runs last, as expected
}

wrong().then(() => wait(500)).then(right);

// Output:
// forEach finished?
// forEach item 1
// forEach item 2
// forEach item 3
// for...of item 1
// for...of item 2
// for...of item 3
// for...of finished

Use for...of when items must run one after another. Use await Promise.all(ids.map(async (id) => { ... })) when they can run in parallel.

Practice exercise

Write an async function makeBreakfast() that uses a wait helper to "boil water" (1 second), then "make tea" (1 second), and in parallel with the tea, "toast bread" (1.5 seconds). Log each step and measure the total time with console.time. Then make the toast step throw an error and catch it with try/catch.

Hint: boil water first with await, then use Promise.all for tea and toast.

Key takeaways

  • An async function always returns a promise.
  • await pauses only that function until the promise settles, then gives you the value.
  • Use try/catch/finally to handle errors from awaited promises.
  • Use Promise.all to run independent tasks in parallel and save time.
  • Never forget await, and don't use await inside forEach; use for...of instead.