Module 5 · Asynchronous JavaScript ⏱ 19 min

Async/Await

By the end of this lesson you will be able to:
  • Mark a function as async and explain what it returns
  • Pause execution with await until a Promise settles
  • Recognise that await outside an async function is a syntax error

Promise chains work, but they read backwards. A sequence of three dependent tasks becomes a staircase of .then() calls, and your eye has to zig-zag down the page to follow the story. Worse, error handling splits between .then() and .catch(), so the happy path and the sad path live in two different places.

Async/await is syntactic sugar over Promises. It lets you write asynchronous code that reads like ordinary synchronous code. An async function can contain await expressions, and each await pauses the function until the Promise settles, then resumes with the resolved value. The result is flatter, left-to-right code where try/catch can handle both success and failure in one place.

Under the hood nothing changes — you are still using Promises. The engine wraps your function's return value in a Promise and schedules the pauses for you. What changes is the shape of the code you read and write.

flowchart LR
  C["caller: const p = fetchData()"] -->|"immediately"| P["Promise { pending }"]
  P -->|"later: settles"| R["resolved value"]
  style P fill:#e0900b,color:#fff
  style R fill:#1e7f3d,color:#fff
An async function immediately returns a Promise. The caller gets that Promise while the body runs later.

The two keywords

Write async before a function definition — async function fetchData() — and the function becomes an async function. The body can now use await, and the function always returns a Promise, even if you write a plain value like return 42.

await sits before a Promise — const data = await somePromise — and does two things. First, it pauses the async function, letting other code run. Second, when the Promise resolves, it resumes the function and gives back the resolved value. If the Promise rejects, await throws the rejection as an exception, which you can catch with try/catch.

This pause is local to the async function. The rest of your program — timers, event handlers, other async functions — keeps running. await is cooperative multitasking, not a lock.

Await a delayed value. The function pauses, then resumes.
function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function greet() {
  const name = await delay(10, "Ada");
  console.log("Hello, " + name);
  return "done";
}

greet();
console.log("first");

What async really returns

An async function wraps whatever you return in a Promise. Write return 7 and the caller receives a Promise that resolves to 7. This is easy to forget when you call an async function from ordinary code: the value you get back is a Promise object, not the number 7.

To use the result you must either await it from another async function or attach .then() to the Promise. Mixing the two styles is common in real codebases — an async function internally uses await, but a non-async caller uses .then() because it cannot use await.

One more rule: await is only legal inside an async function. Try to use it at the top level of a regular function and JavaScript throws a syntax error. This is why a function that needs to wait on asynchronous work must be declared async.

flowchart TD
  S["start greet()"] --> A["await delay(10, 'Ada')"]
  A -->|"pause greet()"| O["other code runs<br/>console.log('first')"]
  O -->|"delay resolves"| R["resume greet()<br/>console.log('Hello, Ada')"]
  style A fill:#e0900b,color:#fff
  style O fill:#3776ab,color:#fff
  style R fill:#1e7f3d,color:#fff
await suspends the async function, lets other tasks run, then resumes when the Promise settles.
Sequential await: each step waits for the previous one.
function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function steps() {
  const a = await delay(5, 1);
  const b = await delay(5, 2);
  const c = await delay(5, 3);
  console.log(a + b + c);
}

steps();

Sequential vs parallel

When you write await one after another, the steps run sequentially. The second delay does not start until the first one finishes. That is correct when step B needs the result of step A, but wasteful when the two tasks are independent.

If the tasks do not depend on each other, start them both and use Promise.all to wait for both together. We cover Promise.all in a later lesson; for now, remember that a string of await statements is a deliberate choice to serialize work, not a free performance boost.

Also remember that await only pauses the async function it sits in. If two different async functions are running, they interleave freely — one pauses at its await while the other runs to its own await. This is what makes concurrent programming possible without threads.

flowchart TD
  Q{"Inside an async function?"} -- Yes --> A["await is legal"]
  Q -- No --> E["SyntaxError"]
  style A fill:#1e7f3d,color:#fff
  style E fill:#b91c1c,color:#fff
await is only legal inside an async function; outside, it is a syntax error.
An async function's return value is a Promise, even when you return a plain number.
async function getNumber() {
  return 7;
}

const p = getNumber();
console.log(p instanceof Promise);
console.log(typeof p);
p.then((v) => console.log("resolved:", v));
Exercise

What does this print? Remember that await pauses only the async function, not the whole program.

async function main() {
  console.log("A");
  await new Promise((r) => setTimeout(r, 5));
  console.log("B");
}

main();
console.log("C");
Exercise

Write an async function delayedGreet(name, ms) that waits ms milliseconds using setTimeout wrapped in a Promise, then returns the string "Hello, <name>!".

async function delayedGreet(name, ms) {
  // wait ms milliseconds, then return "Hello, <name>!"
}
Exercise

This function tries to use await but forgets the async keyword, so it throws a syntax error. Fix it by adding the one keyword that makes await legal.

function waitAndDouble(ms, n) {
  await new Promise((resolve) => setTimeout(resolve, ms));
  return n * 2;
}
Exercise

What does this print? An async function always returns a Promise, even when the return value looks like a plain number.

async function give() {
  return 5;
}

const x = give();
console.log(x instanceof Promise);
console.log(typeof x);
Exercise

Write sumAfterDelays(a, b) as an async function that awaits a 5-millisecond delay, then returns a + b. The delay itself is new Promise((resolve) => setTimeout(resolve, 5)).

async function sumAfterDelays(a, b) {
  // await a 5ms delay, then return a + b
}

Recap

  • async function declares a function that can use await and always returns a Promise.
  • await promise pauses the async function until the Promise settles, then gives back the resolved value.
  • The pause is local: other code and other async functions keep running.
  • An async function wraps its return value in a Promise — unwrap it with .then() or another await.
  • await outside an async function is a syntax error.
  • Sequential await statements run one after another; use Promise.all when tasks are independent.

Next you will learn how to catch and recover when a Promise rejects, both with .catch() and with try/catch around await.

Checkpoint quiz

What does an async function return if you write return 42 inside it?

Where is await legal?

Go deeper — technical resources