JAVASCRIPT / PROMISES AND ASYNC PATTERNS
async and await for linear-looking code
Write asynchronous steps as straight-line code with async and await, catch failures with try/catch, and spot awaits that needlessly serialize work.
What you will learn
- Use await to read a promise's value as a plain expression result
- Handle async failures with ordinary try/catch/finally around await
- Explain why an async function returns before its body has finished
- Start independent promises before awaiting them to avoid serializing
Understanding async and await for linear-looking code
The `async` keyword marks a function whose body is allowed to pause, and `await` is where it pauses. A useful mental model is that the engine cuts the body into segments at every `await`: the first segment runs synchronously the moment you call the function, and each later segment is registered as a continuation of the promise being awaited. That is why `await` suspends only the one function containing it and never blocks the program.
Because the caller keeps running, an async function has to hand back something immediately, so it always returns a promise. `return value` inside the body fulfils that promise, and an uncaught `throw` rejects it. In the other direction, a rejected promise reaching an `await` is re-thrown at that exact spot, which is what makes plain `try`, `catch`, and `finally` work again across asynchronous boundaries instead of the error-first callback argument they replaced.
The price of the linear look is accidental serialization. `await` does not start any work; the expression next to it does, when the promise is constructed. If line two calls `fetchPosts()` after line one awaits `fetchUser()`, then `fetchPosts` has not even been called while the user request is in flight. When two operations do not depend on each other, create both promises first and await them afterwards.
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
async function loadProfile(id) {
console.log("start", id);
const user = await delay(20, { id, name: "Ada" });
console.log("got user", user.name);
const posts = await delay(10, ["intro", "notes"]);
console.log("got posts", posts.length);
return `${user.name} has ${posts.length} posts`;
}
const pending = loadProfile(7);
console.log("call returned a promise:", pending instanceof Promise);
pending.then(result => console.log(result));
console.log("caller keeps going");await pauses only the async function that contains it, converting a promise's eventual value into a normal expression result while the caller and the rest of the program continue.
Worked examples
try/catch/finally around await
A rejected promise is thrown at the await, so synchronous error handling applies, and returning from catch fulfils the async function.
function failAfter(ms, message) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
}
async function readConfig() {
try {
const base = await Promise.resolve("base config");
console.log("loaded", base);
await failAfter(5, "disk offline");
console.log("this line is skipped");
} catch (err) {
console.log("caught:", err.message);
return "fallback config";
} finally {
console.log("finally runs either way");
}
}
readConfig().then(value => console.log("resolved with:", value));Example explained
Line 1The rejection of `failAfter` is converted into a thrown `Error` at the await, so the `catch` block sees it like any synchronous throw.
Line 2`console.log("this line is skipped")` never runs because the throw happens before that segment of the body resumes.
Line 3`return "fallback config"` from inside `catch` fulfils the promise `readConfig()` gave back, so the caller sees success, not failure.
Line 4`finally` runs after the return value is evaluated but before the promise settles, which is why it logs before `resolved with:`.
await waits, the call starts
Shows that moving the calls above the awaits lets two independent operations overlap, with no change to the awaits themselves.
function job(name, ms) {
console.log("start " + name);
return new Promise(resolve => setTimeout(() => {
console.log("done " + name);
resolve(name);
}, ms));
}
async function sequential() {
await job("A", 20);
await job("B", 10);
console.log("sequential finished");
}
async function overlapping() {
const p1 = job("C", 20);
const p2 = job("D", 10);
await p1;
await p2;
console.log("overlapping finished");
}
sequential().then(overlapping);Example explained
Line 1`start B` appears only after `done A` because the call `job("B", 10)` is part of a body segment that has not run yet.
Line 2In `overlapping`, both calls happen before any await, so both timers are already counting down together.
Line 3`done D` precedes `done C` since D's timer is shorter, even though the code awaits `p1` first.
Line 4Awaiting `p2` after it already settled still costs one microtask hop, but no extra waiting time.
What async returns and what await accepts
An async function with no await still returns a promise, and await works on plain values and on any object with a then method.
async function double(n) {
return n * 2;
}
const thenable = {
then(resolve) { resolve("not a real promise"); }
};
async function main() {
const p = double(4);
console.log(p instanceof Promise, typeof p.then);
console.log(await p);
console.log(await 7);
console.log(await thenable);
}
main();Example explained
Line 1`double` contains no await, yet `double(4)` is a promise: the `async` keyword alone guarantees that.
Line 2`await p` yields `8`, the fulfilment value, so the promise wrapper is invisible to the surrounding code.
Line 3`await 7` is legal; a non-promise is adopted as an already-fulfilled value and handed straight back.
Line 4`await thenable` calls that object's `then` method, so await follows the thenable protocol rather than checking for a Promise instance.
Important notes
`await` is only valid directly inside an async function or at the top level of an ES module; inside a non-async callback such as an array method's arrow function it is a syntax error.
An await always defers the rest of the function to a later microtask, even when the awaited value is already settled or not a promise at all, so code after an await never runs in the same synchronous turn.
Common mistakes
Omitting `await` and using the result as data: `const user = getUser(); user.name` is `undefined` because `user` is a promise, and any later rejection surfaces as an unhandled rejection far from the bug.
Writing `array.forEach(async item => { await save(item); })` and assuming the code after the loop runs last; `forEach` discards the returned promises, so the loop finishes instantly while the saves are still pending.
Putting `try { doWork(); } catch {}` around an async call without awaiting it: the `try` block has already exited when the promise rejects, so the catch never fires.
Try it yourself
Change, predict, then run
Write `delay(ms, label)` returning a promise, then an async `report()` that awaits `delay(300, "A")` and `delay(300, "B")` in turn and logs `Date.now() - start`. Rewrite it so both delays begin before the first await and confirm the logged time drops from roughly 600 to roughly 300.
Open the JavaScript workspaceCheck your understanding
Two independent operations each take 200 ms. Why does `const a = await slow(); const b = await slow();` take about 400 ms, while `const p1 = slow(); const p2 = slow(); const a = await p1; const b = await p2;` takes about 200 ms?
- In the first version the second `slow()` call is not evaluated until the first await resumes, so that operation starts 200 ms late
- In the second version the two awaits execute simultaneously, which lets the engine run the operations in parallel
- `await` runs its operation on a background thread only when more than one promise exists at the same time
- Storing a promise in a variable lets the engine reuse a cached result instead of doing the work twice
Show answer
The work starts when `slow()` is called and its promise is constructed; `await` only decides when the async function resumes. Option two is tempting but wrong: the second version still awaits `p1` and then `p2` strictly in order, so the awaits are not simultaneous; the overlap comes entirely from both calls being made before either await.