Promises & async/await
Chaining, error handling, parallel vs sequential, withResolvers.
A promise is a value that's "not here yet, but will be." Async/await is fancy syntax that lets you write code that uses promises as if it were synchronous. Together they replaced the callback nightmare of 2010s JavaScript with something you can actually read.
Promise states
Every promise is in exactly one of three states:
- pending - work is still in flight
- fulfilled- work succeeded, here's a value
- rejected- work failed, here's the reason
A promise moves from pending to fulfilled or rejected exactly once, then never changes again.
Creating and consuming a promise
// Most of the time you get promises from APIs (fetch, etc.)
// But you can build your own:
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 500);
});
// Consume with .then / .catch / .finally
p
.then(value => console.log("got:", value))
.catch(err => console.error("oops:", err))
.finally(() => console.log("cleanup"));.then returns a new promise, so you can chain. Each .then waits for the previous one to resolve and passes its return value down.
async / await: the readable version
An async function always returns a promise. await pauses inside it until a promise resolves. The same code with promises and with async/await:
// .then chain
function loadUser(id) {
return fetch("/api/users/" + id)
.then(r => r.json())
.then(user => fetch("/api/orders/" + user.id))
.then(r => r.json());
}
// async/await
async function loadUser(id) {
const userRes = await fetch("/api/users/" + id);
const user = await userRes.json();
const orderRes = await fetch("/api/orders/" + user.id);
return orderRes.json();
}Error handling with try/catch
async function load(id) {
try {
const res = await fetch("/api/users/" + id);
if (!res.ok) throw new Error("Bad status: " + res.status);
return await res.json();
} catch (err) {
console.error("failed:", err);
return null;
}
}try/catch around await catches both thrown errors and rejected promises. One pattern for both.The sequential await anti-pattern
Each awaitwaits for the previous one. If your awaits don't depend on each other, that's a problem:
// SLOW: 3 seconds total (1s + 1s + 1s)
async function loadAllSlow() {
const a = await fetch("/a"); // wait 1s
const b = await fetch("/b"); // then wait 1s
const c = await fetch("/c"); // then wait 1s
return [a, b, c];
}
// FAST: ~1 second total (all three in parallel)
async function loadAllFast() {
const [a, b, c] = await Promise.all([
fetch("/a"),
fetch("/b"),
fetch("/c"),
]);
return [a, b, c];
}Promise.all, allSettled, race, any
// all: resolves when ALL succeed, rejects on first failure
Promise.all([p1, p2, p3])
// allSettled: never rejects - gives an array of { status, value/reason }
Promise.allSettled([p1, p2, p3]).then(results => {
results.forEach(r => {
if (r.status === "fulfilled") console.log("ok:", r.value);
else console.log("err:", r.reason);
});
});
// any: resolves with first SUCCESS, only rejects if all fail
Promise.any([p1, p2, p3])
// race: resolves OR rejects with first to settle (good for timeouts)
Promise.race([p1, p2, p3])all- must all succeed (e.g. parallel data fetch).allSettled- "try everything, tell me what happened" (e.g. batch jobs).any- first one that works (e.g. fallback mirrors).race- first one to settle, win or lose (e.g. timeout vs fetch).
Promise.withResolvers: a new convenience
Sometimes you want a promise plus the resolve and rejecthandles to call later from somewhere else. Before, you had to capture them inside the constructor. Now there's a one-liner (ES2024):
const { promise, resolve, reject } = Promise.withResolvers();
button.addEventListener("click", () => resolve("clicked!"));
const result = await promise;Try it: parallel vs sequential timing
The playground below has a sleep helper. Run it and watch the timings. Then try changing it to run jobs sequentially and see the total go up.
Quiz
What does an `async` function always return?
Recap
- Promises move from pending to fulfilled or rejected exactly once.
asyncfunctions return promises;awaitunwraps them.- Use
try/catchwithawaitfor error handling. - Don't await independent jobs in sequence. Use
Promise.allfor parallel. all,allSettled,any,raceeach have a clear job. Pick the one that matches your intent.