Promises Refresher
A Promise is an object representing a value that may not exist yet — the eventual result (or failure) of an asynchronous operation. Promises replaced callback hell with a flat, composable, chainable model, and they are the foundation async/await is built on. Understanding them directly — not just through await — is what lets you coordinate concurrency correctly.
The three states
State | Meaning |
|---|---|
pending | The operation has not finished yet |
fulfilled | It succeeded — the promise has a value |
rejected | It failed — the promise has a reason (error) |
A promise settles once: pending → fulfilled, or pending → rejected. After that it is frozen — its value or reason never changes, and any handler attached later still receives it. "Resolved" loosely means settled; a promise can also resolve to another promise and adopt its eventual state.
Consuming a promise
fetch('https://api.example.com/users')
.then((response) => response.json()) // runs on success, returns a NEW promise
.then((users) => console.log(users)) // chained: receives the json
.catch((err) => console.error(err)) // runs if ANY step above rejects
.finally(() => console.log('done')) // always runs, value untouchedCreating a promise
Wrap a callback-style or timer-based operation in the Promise constructor. You get resolve and reject — call exactly one, once:
function wait(ms) {
return new Promise((resolve, reject) => {
if (ms < 0) return reject(new Error('ms must be >= 0'))
setTimeout(() => resolve(`waited ${ms}ms`), ms)
})
}
wait(500).then((msg) => console.log(msg))Combinators — running promises together
The real power shows when coordinating multiple async operations. These four static methods cover almost every concurrency need:
// Run in PARALLEL, wait for all; reject as soon as ANY rejects const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]) // Like all(), but NEVER rejects — reports each outcome individually const results = await Promise.allSettled([fetchA(), fetchB()]) // First to SETTLE (fulfill OR reject) wins — good for timeouts const fastest = await Promise.race([primary(), timeout(5000)]) // First to FULFILL wins; rejects only if ALL reject const ok = await Promise.any([mirror1(), mirror2()])
Method | Resolves when | Rejects when |
|---|---|---|
| All fulfill | Any rejects (fail-fast) |
| All settle | Never |
| First settles | If the first to settle rejects |
| First fulfills | All reject ( |
The microtask timing detail
Promise callbacks run as microtasks — after the current synchronous code, but before timers and I/O. This ordering explains a lot of "why did this log first?" puzzles:
console.log('1: sync')
Promise.resolve().then(() => console.log('3: microtask'))
setTimeout(() => console.log('4: timer'), 0)
console.log('2: sync')1: sync 2: sync 3: microtask 4: timer
Full ordering rules — including process.nextTick — are in setImmediate & process.nextTick.
Common mistakes
Forgetting to return inside
.then()— the next step receivesundefined, and errors escape the chain.Not handling rejections — an unhandled rejection terminates the Node process by default (since v15).
Nesting
.then()inside.then()— recreates the pyramid; chain orawaitinstead.Running sequentially when you could parallelize — independent work belongs in
Promise.all.Mixing
awaitand.then()on the same promise — pick one style per operation for clarity.