04 · Promises & Async/Await¶
This module builds on the event loop and microtask queue from Module 3 — Promises are the mechanism that schedules work onto that microtask queue.
What a Promise represents¶
A Promise is an object representing a value that may not be available yet. It's always in one of three states: pending, fulfilled, or rejected — and once it settles (fulfills or rejects), it never changes again.
const willSucceed = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done!"); // moves the promise to "fulfilled"
}, 500);
});
willSucceed.then((value) => console.log(value)); // done! (after ~500ms)
Creating a Promise around async work¶
Wrap any callback-based operation in new Promise to give it a .then/
.catch interface, calling resolve on success and reject on failure.
function delay(ms, value) {
return new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error("delay must be non-negative"));
return;
}
setTimeout(() => resolve(value), ms);
});
}
delay(100, "hello")
.then((result) => console.log(result)) // hello
.catch((error) => console.log(error.message));
delay(-1, "hello").catch((error) => console.log(error.message));
// delay must be non-negative
Chaining .then¶
Each .then returns a new Promise, so steps that depend on the previous
result can be chained instead of nested — this directly replaces the
callback pyramid from Module 3.
function getUser(id) {
return delay(100, { id, name: "Ada" });
}
function getOrders(user) {
return delay(100, [`order-1 for ${user.name}`, `order-2 for ${user.name}`]);
}
getUser(1)
.then((user) => getOrders(user))
.then((orders) => console.log(orders[0])) // order-1 for Ada
.catch((error) => console.log("something failed:", error.message));
async/await¶
async/await is syntax sugar over Promises: an async function always
returns a Promise, and await pauses execution inside it until the awaited
Promise settles — without blocking the rest of the program.
async function loadOrderSummary() {
const user = await getUser(1); // pauses here until resolved
const orders = await getOrders(user); // then continues
return `${user.name}: ${orders.length} orders`;
}
loadOrderSummary().then((summary) => console.log(summary));
// Ada: 2 orders
The same logic written with raw .then chains is functionally identical,
but await reads top-to-bottom like synchronous code.
Error handling with try/catch¶
Wrap await calls in try/catch exactly like synchronous code — this is
the big ergonomic win over .then().catch() chains for multi-step logic.
async function safeLoadOrderSummary(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user);
return `${user.name}: ${orders.length} orders`;
} catch (error) {
console.log("failed to load summary:", error.message);
return null;
}
}
safeLoadOrderSummary(1).then((summary) => console.log(summary));
// Ada: 2 orders
Running promises in parallel¶
Sequential await calls that don't depend on each other waste time. Kick
off independent operations together and await them as a group instead.
async function loadDashboard() {
// Sequential — slower, waits for each one before starting the next
const a = await delay(200, "profile");
const b = await delay(200, "settings");
// total: ~400ms
// Parallel — starts both immediately, waits for whichever is slower
const [profile, settings] = await Promise.all([
delay(200, "profile"),
delay(200, "settings"),
]);
// total: ~200ms
return { a, b, profile, settings };
}
loadDashboard().then((result) => console.log(result));
Promise.all vs. Promise.allSettled¶
Promise.all rejects as soon as any promise rejects, discarding the
others' results. Promise.allSettled always waits for every promise and
reports each outcome individually — useful when partial failure is fine.
const tasks = [delay(50, "ok-1"), Promise.reject(new Error("task failed")), delay(50, "ok-2")];
Promise.all(tasks).catch((error) => console.log("all() rejected:", error.message));
// all() rejected: task failed
Promise.allSettled(tasks).then((results) => console.log(results));
// [
// { status: 'fulfilled', value: 'ok-1' },
// { status: 'rejected', reason: Error: task failed },
// { status: 'fulfilled', value: 'ok-2' }
// ]
Promise combinators cheat sheet¶
| Method | Resolves when | Rejects when |
|---|---|---|
Promise.all |
every promise fulfills | any single promise rejects (fails fast) |
Promise.allSettled |
every promise settles (fulfilled or rejected) | never — always fulfills with a status report |
Promise.race |
the first promise to settle, settles | the first promise to settle rejects |
Promise.any |
the first promise to fulfill, fulfills | only if all promises reject |
How It Actually Works¶
A Promise is a state machine (pending → fulfilled/rejected, one-way, settled
once) that maintains an internal list of reaction callbacks. Calling .then doesn't
run your callback synchronously even if the promise is already resolved — the spec
requires it to be scheduled as a microtask, via queueMicrotask internally.
Microtasks run in their own queue that the event loop fully drains after every
single callback (macrotask) finishes, and before the next macrotask (timer, I/O, render)
is allowed to run. That's the concrete reason Promise.resolve().then(fn) always runs
before setTimeout(fn, 0), no matter what: the microtask queue is checked and emptied
first, every time, even if new microtasks are added while draining it.
async/await is syntax sugar over generators and promises: V8 desugars an async
function into something conceptually like a generator function driven by an internal
runner that calls .next() each time, and every await expr is transformed into
yield-ing control back to that runner while chaining .then() onto
Promise.resolve(expr). This is why await a non-promise value still takes at least
one microtask tick to resume — even await 5 schedules a microtask — and why an
async function always returns a promise, even if you return a plain value: the
runner wraps whatever comes out in Promise.resolve(...) before handing it back.
Exercise¶
Write an async function fetchWithTimeout(promise, ms) that returns
whatever promise resolves to, but rejects with new Error("timed out") if
it takes longer than ms milliseconds. Hint: use Promise.race with a
second promise that rejects after a setTimeout.