Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ES2017+ ~27 min· topic 8 of 8

Topic 10.8

async and await

In one line

async functions always return a promise, and inside them await pauses that function (not the page) until a promise settles, so async code reads top to bottom with ordinary try...catch; start independent jobs before awaiting them to keep them parallel.

Think of it like this

Bookmarking a book. You're reading a recipe book and step 4 says "let the dough rise for an hour". You put a bookmark in (that's await), go and do other things, and when the hour is up you open the book at the bookmark and carry on from exactly that line. The house didn't stop for an hour; only that one recipe paused.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

async function
A function marked async; it always returns a promise.
await
Pause this async function until a promise settles, then give its value (or throw its error).
Resume
Carry on running a paused async function from the line after the await.
Sequential
One after another: the second job starts only when the first is done.
Parallel (here)
Started together so their waiting overlaps.
Top-level await
Using await outside any function, allowed only in ES modules.
ES module
A JavaScript file loaded with type="module" or import; it has its own scope and can use import/export.

Step by step

01From a chain to await

Here is the same flow twice: first as a promise chain, then with await. The second reads like synchronous code.

compare.jswhole filejavascript
function showOrders() {
  return loadUser(7)
    .then((user) => loadOrders(user.id))
    .then((orders) => render(orders))
    .catch((err) => showError(err));
}

async function showOrders2() {
  try {
    const user = await loadUser(7);
    const orders = await loadOrders(user.id);
    render(orders);
  } catch (err) {
    showError(err);
  }
}

02What happens at an await

The function runs until the first await, then gives control back. The caller receives a pending promise and continues. When the awaited promise settles, the rest of the function is queued as a microtask and resumes.

What happens at an awaitdiagram
Rendering diagram…

03Errors become exceptions

A rejected promise makes await throw, so try...catch works. finally always runs, which is perfect for resetting a button or hiding a spinner.

errors.jswhole filejavascript
async function save() {
  button.disabled = true;
  try {
    await sendToServer();
    status.textContent = "Saved";
  } catch (err) {
    status.textContent = "Could not save: " + err.message;
  } finally {
    button.disabled = false;
  }
}

04Start first, await later

Calling getUser() starts the work. Awaiting it is a separate step. Start independent jobs together, then await them together.

parallel.jswhole filejavascript
// slow: about 300 + 300 ms
const user = await getUser();
const posts = await getPosts();

// fast: about 300 ms
const [user2, posts2] = await Promise.all([getUser(), getPosts()]);

05Loops: for...of waits, forEach doesn't

for (const id of ids) { await save(id); } saves one at a time, in order. ids.forEach(async (id) => { await save(id); }) starts them all and returns immediately: forEach ignores the promises its callback returns, so you can't know when they're done or catch their errors.

06Top-level await in modules

In a module you can await at the top level, which is handy for loading configuration before the rest of the module runs.

config.jswhole filejavascript
const response = await fetch("/config.json");
export const config = await response.json();

Try it yourself

  1. 1

    Throw from an async function

    In the first example, add throw new Error("nope") as the first line of getGreeting. Predict which letters still print and what red line appears.

  2. 2

    Make it sequential by accident

    In the third example, rewrite parallel as const c = job("C", 150); await c; const d = job("D", 150); await d;. Predict the log order and the boolean.

  3. 3

    Start first, await later

    Now rewrite it as const c = job("C", 150); const d = job("D", 150); await c; await d;. Is it parallel again? Predict before you run.

Code & diagrams

async returns a promise, await pauses only the function New tab

Even though `getGreeting` had its answer immediately, the code after `await` still waited until the synchronous code (B) finished.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

getGreeting() returns a Promise? true
A: main starts (synchronously)
B: the caller carries on while main waits
C: main resumes and got hello
D: main's promise fulfilled with HELLO
try, catch and finally with await New tab

The rejected promise made `await` throw, so the ordinary `catch` handled it. `finally` ran both times. Try the buttons too.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Loaded: { name: 'Asha', points: 120 }
Finished (ok = true)
Caught: Server said 503
Finished (ok = false)
Sequential vs parallel New tab

In the sequential version, B doesn't even start until A is done. In the parallel one, both start before either is awaited.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

start A
done A
start B
done B
Sequential took at least 290 ms? true
start C
start D
done C
done D
Parallel took under 290 ms? true
Top-level await in a module script ES2022+ New tab

This only works because the script is `type="module"`. Remove that and the browser reports a `SyntaxError`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

module starts
after top-level await: { theme: 'dark', lang: 'en' }

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

await outside an async function

In a classic script, write const user = await loadUser(7); at the top level, or inside a normal function.

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

Break #2

await inside forEach

Write items.forEach(async (item) => { await save(item); }); console.log("All saved");.

terminal
$ Console
── what you'll see ──
All saved (printed before any save has finished; errors show as Uncaught (in promise))

Break #3

Forget the await

Write const user = getUserAsync(); console.log(user.name);.

terminal
$ Console
── what you'll see ──
undefined

Myth vs fact

Myth

await blocks the page until the promise settles.

Fact

It pauses only the async function it's in. The rest of the page, including clicks and other code, keeps running.

Myth

async/await replaces promises.

Fact

It's built on promises: async functions return promises, and await waits for them. You still need Promise.all and friends for parallel work.

Myth

Marking a function async makes its code run in the background.

Fact

Everything up to the first await runs synchronously, on the main thread, right when you call it. A heavy loop in an async function still blocks.

Myth

return await is always pointless.

Fact

Inside try...catch, return await p makes rejections of p land in your catch; plain return p doesn't. Outside try, the two behave the same.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    An async function's code after each await resumes as a microtask, so a long run of awaits on already-resolved values never lets the browser paint. To really yield to rendering and input, await a task, such as await new Promise((r) => setTimeout(r, 0)), or scheduler.yield() where supported.

  • ▸

    Unhandled rejections from fire-and-forget calls are the main async/await bug. Either await every async call, attach .catch, or deliberately mark it (void doIt().catch(report)). ESLint's no-floating-promises rule (typescript-eslint) and require-await catch these mistakes.

  • ▸

    for await (const chunk of stream) (ES2018) consumes async iterables such as a ReadableStream (iterable in modern browsers) or an async generator (async function*). Top-level await (ES2022) blocks the evaluation of modules that import yours, so keep it for real setup, not slow network calls on the critical path.

  • ▸

    When you need to start work now and handle its result later, keep the promise in a variable: starting is calling, awaiting is a separate decision. Awaiting promises one by one after starting them all is parallel, but if an early one rejects while you're still awaiting another, its rejection may be reported as unhandled until you reach it. Promise.all avoids that.

Remember this

  1. 1

    Putting async before a function (async function load() {}, async () => {}, or an async method) makes it an async function. It always returns a promise: returning a value fulfils the promise with that value, and throwing an error rejects it. So callers use it with .then or with await. async/await arrived in ES2017 and are built on promises; they're a nicer way to write the same thing.

  2. 2

    await expression can be used inside an async function. If the expression is a promise, the function pauses at that line until the promise settles. If it fulfils, await gives you the value. If it rejects, await throws the reason right there, as if that line had thrown. Awaiting a non-promise value works too; it simply continues on the next microtask.

  3. 3

    Pausing doesn't block anything else. At an await, the async function returns control to its caller (the caller gets the still-pending promise), the rest of the page carries on, and the function resumes later as a microtask (Topic 10.3) when the awaited promise settles. Code before the first await runs synchronously, like any function call.

  4. 4

    Because a rejected await throws, you handle errors with ordinary **try...catch...finally**: put the awaits in try, handle failures in catch, and clean up (hide a spinner) in finally. A rejection you don't catch makes the async function's own promise reject, so the error travels to whoever awaits it. If nobody does, you get an unhandled rejection (Topic 10.6).

  5. 5

    Sequential vs parallel: await a(); await b(); waits for a to finish before even starting b. That's right when b needs a's result, but slow when they're independent. To run independent jobs together, start them first and await them together: const [a, b] = await Promise.all([getA(), getB()]);. The total time becomes the slowest job instead of the sum.

  6. 6

    Top-level await (ES2022) lets you use await outside any function, but only at the top level of an ES module (<script type="module">, Phase 12). In a classic script it's a SyntaxError. The module pauses, and any module that imports it waits too. Two more related tools: for await (const x of source) (ES2018) loops over values that arrive asynchronously, and array methods like forEach don't wait for async callbacks, so use for...of with await inside, or Promise.all with map.

Explain it without notes

01

What does an async function return, and what does await do?

02

Does await block the main thread? Explain what really happens.

03

How do you handle errors with async/await, and what happens to an uncaught one?

04

How do you run two independent async jobs in parallel with await, and why is await a(); await b(); slower?

05

What is top-level await, where can you use it, and what is its cost?

Practice

01

Rewrite this with async/await and try...catch: getUser().then((u) => getPosts(u.id)).then((p) => console.log(p.length)).catch((e) => console.log("Error:", e.message));

02

Write async function loadAll(ids, load) that loads every id in parallel and returns an array of results in the same order.

03

Write async function retry(fn, times) that calls the async function fn up to times times, returning the first success and throwing the last error if all fail.

Trade-offs

  • ↔

    async/await reads like synchronous code and uses normal try...catch, but makes it easy to accidentally serialise independent work; promise chains and combinators make the flow of concurrency more visible.

  • ↔

    Top-level await simplifies module setup but delays every importer; an exported init() function gives callers control over when to wait, at the cost of more code.

Done when you can

  • I know an async function always returns a promise.

  • I can use await and explain that it pauses only its function.

  • I handle async errors with try...catch...finally.

  • I start independent jobs together and await them with Promise.all.

  • I use for...of (not forEach) when I need to await inside a loop.

  • I know top-level await works only in modules.