CodingNic

Asynchronous JavaScript

async/await

Asynchronous JavaScript 25 min read

async/await

Objectives

By the end of this chapter, you should be able to:

  • Mark a function async and use await inside it
  • Explain what an async function actually returns
  • Rewrite a promise chain using await instead of .then()
  • Explain the difference between awaiting promises one at a time and running them together

💡 Why this matters: async/await is how most real JavaScript code works with promises today. It doesn’t replace promises, it’s built directly on top of them, but it reads far closer to ordinary, top-to-bottom code.

The async Keyword

Adding async before a function definition changes two things: you can use await inside it, and the function automatically returns a promise, even if you return a plain value.

javascript
async function getFive() {
  return 5;
}

getFive().then((value) => {
  console.log(value);
});
// 5

getFive() looks like it returns 5 directly, but calling it actually gives you a promise that resolves to 5. That’s what makes .then() work on the result.

The await Keyword

Inside an async function, await pauses that function until the promise on its right resolves, then hands you the resolved value directly, no .then() needed.

javascript
function delayedValue(value, ms) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function getUser() {
  const user = await delayedValue({ name: "Sam" }, 1000);
  console.log("Got user:", user.name);
}

getUser();
// (one second later) Got user: Sam

Nothing else on the page freezes while await waits, only the code inside getUser() pauses at that line. This is the same non-blocking behavior from the Timers lesson, await just makes it look like ordinary sequential code.

await only works inside a function marked async. Using it anywhere else is a syntax error.

Rewriting a Promise Chain

Compare this to the .then() chain from last lesson, doing the exact same three sequential steps:

javascript
async function runSteps() {
  const r1 = await delayedValue("Step 1 done", 1000);
  const r2 = await delayedValue(r1 + " -> Step 2 done", 1000);
  const r3 = await delayedValue(r2 + " -> Step 3 done", 1000);
  console.log("Chain result:", r3);
}

runSteps();
// (three seconds later) Chain result: Step 1 done -> Step 2 done -> Step 3 done

Same behavior, same timing, but each step reads as a plain line of code, assign a variable, use it on the next line. No .then(), no nested or nesting-adjacent callbacks.

Sequential vs. Parallel: A Real Gotcha

Awaiting promises one after another runs them one after another, even if they don’t depend on each other.

javascript
async function sequential() {
  const start = Date.now();
  await delayedValue("a", 1000);
  await delayedValue("b", 1000);
  console.log("took about", Date.now() - start, "ms");
}

sequential();
// took about 2000 ms

Each await waits for the previous line to finish before starting the next one, even though "a" and "b" have nothing to do with each other. Since they’re independent, they could run at the same time instead. Combine await with Promise.all() (previous lesson) to do that:

javascript
async function parallel() {
  const start = Date.now();
  await Promise.all([delayedValue("a", 1000), delayedValue("b", 1000)]);
  console.log("took about", Date.now() - start, "ms");
}

parallel();
// took about 1000 ms

Both versions get the same two results. The sequential version takes roughly the sum of both delays, the parallel version takes roughly the longer of the two, since both start at the same time. Reach for Promise.all() whenever you’re awaiting several promises that don’t depend on each other’s results.

A First Look at Error Handling

await on a rejected promise throws, which means ordinary try/catch (Course 1, Error Handling) works with it directly.

javascript
function delayedFailure(ms) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error("failed")), ms));
}

async function tryIt() {
  try {
    await delayedFailure(1000);
  } catch (error) {
    console.log("Caught:", error.message);
  }
}

tryIt();
// (one second later) Caught: failed

This is covered in full next lesson, for now, know that try/catch is the standard way to handle a rejected await inside an async function.

Try It

Starter code for all four exercises:

javascript
function delayedValue(value, ms) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
  1. Write an async function that awaits delayedValue("done", 500) and logs the result.
  2. Write an async function with three sequential await calls to delayedValue(), each building on the previous result, matching the chain example above but with your own values.
  3. Write two async functions, one that awaits two delayedValue() calls one at a time, and one that awaits both together with Promise.all(). Log how long each takes and compare.
  4. Write an async function that calls a promise-returning function which always rejects, and handle the rejection with try/catch.

Recap

  • async before a function makes it always return a promise, and allows await inside it.
  • await pauses an async function until a promise resolves, then gives you the value directly, no .then() required.
  • Awaiting promises one at a time runs them sequentially, even if they’re independent. Promise.all() combined with await runs independent promises together, finishing sooner.
  • try/catch handles a rejected await the same way it handles a thrown error.

Next lesson: the Fetch API, using everything from this module to actually request data from a server.