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
awaitoutside any function, allowed only in ES modules. - ES module
- A JavaScript file loaded with
type="module"orimport; it has its own scope and can useimport/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.
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.
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.
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.
// 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.
const response = await fetch("/config.json");
export const config = await response.json();Try it yourself
- 1
Throw from an async function
In the first example, add
throw new Error("nope")as the first line ofgetGreeting. Predict which letters still print and what red line appears. - 2
Make it sequential by accident
In the third example, rewrite
parallelasconst c = job("C", 150); await c; const d = job("D", 150); await d;. Predict the log order and the boolean. - 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
Even though `getGreeting` had its answer immediately, the code after `await` still waited until the synchronous code (B) finished.
Preview
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 HELLOThe rejected promise made `await` throw, so the ordinary `catch` handled it. `finally` ran both times. Try the buttons too.
Preview
Console
Nothing logged yet.Expected console output
Loaded: { name: 'Asha', points: 120 }
Finished (ok = true)
Caught: Server said 503
Finished (ok = false)In the sequential version, B doesn't even start until A is done. In the parallel one, both start before either is awaited.
Preview
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? trueThis only works because the script is `type="module"`. Remove that and the browser reports a `SyntaxError`.
Preview
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.
Break #2
await inside forEach
Write items.forEach(async (item) => { await save(item); }); console.log("All saved");.
Break #3
Forget the await
Write const user = getUserAsync(); console.log(user.name);.
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
awaitresumes 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 asawait new Promise((r) => setTimeout(r, 0)), orscheduler.yield()where supported. - ▸
Unhandled rejections from fire-and-forget calls are the main async/await bug. Either
awaitevery async call, attach.catch, or deliberately mark it (void doIt().catch(report)). ESLint'sno-floating-promisesrule (typescript-eslint) andrequire-awaitcatch these mistakes. - ▸
for await (const chunk of stream)(ES2018) consumes async iterables such as aReadableStream(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.allavoids that.
Remember this
- 1
Putting
asyncbefore a function (async function load() {},async () => {}, or anasyncmethod) 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.thenor withawait.async/awaitarrived in ES2017 and are built on promises; they're a nicer way to write the same thing. - 2
await expressioncan 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,awaitgives you the value. If it rejects,awaitthrows 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
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 firstawaitruns synchronously, like any function call. - 4
Because a rejected
awaitthrows, you handle errors with ordinary **try...catch...finally**: put the awaits intry, handle failures incatch, and clean up (hide a spinner) infinally. 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
Sequential vs parallel:
await a(); await b();waits forato finish before even startingb. That's right whenbneedsa'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
Top-level await (ES2022) lets you use
awaitoutside any function, but only at the top level of an ES module (<script type="module">, Phase 12). In a classic script it's aSyntaxError. 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 likeforEachdon't wait for async callbacks, so usefor...ofwithawaitinside, orPromise.allwithmap.
Explain it without notes
What does an async function return, and what does await do?
Does await block the main thread? Explain what really happens.
How do you handle errors with async/await, and what happens to an uncaught one?
How do you run two independent async jobs in parallel with await, and why is await a(); await b(); slower?
What is top-level await, where can you use it, and what is its cost?
Practice
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));
Write async function loadAll(ids, load) that loads every id in parallel and returns an array of results in the same order.
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/awaitreads like synchronous code and uses normaltry...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
asyncfunction always returns a promise.I can use
awaitand 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(notforEach) when I need to await inside a loop.I know top-level await works only in modules.