NodeJSPromises Refresher

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

JS
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 untouched
Chaining is the superpower
Each `.then()` returns a **new** promise, so steps form a flat sequence instead of a pyramid. Returning a *value* passes it to the next `.then()`; returning a *promise* makes the chain wait for it. A single `.catch()` at the end handles errors from every step above it — like a `try` around the whole chain.
Creating a promise

Wrap a callback-style or timer-based operation in the Promise constructor. You get resolve and reject — call exactly one, once:

JS
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))
The explicit-construction antipattern
Do not wrap something that is *already* a promise in `new Promise`. `new Promise((res) => fetch(url).then(res))` swallows rejections and is pure noise — just return `fetch(url)`. Reach for the constructor **only** to promisify a callback/event API that is not promise-based yet.
Combinators — running promises together

The real power shows when coordinating multiple async operations. These four static methods cover almost every concurrency need:

JS
// 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

Promise.all

All fulfill

Any rejects (fail-fast)

Promise.allSettled

All settle

Never

Promise.race

First settles

If the first to settle rejects

Promise.any

First fulfills

All reject (AggregateError)

all rejects fast but does not cancel
When one promise passed to `Promise.all` rejects, the combined promise rejects *immediately* — but the others **keep running** in the background (JS promises are not cancellable). If their results matter or they hold resources, use `allSettled`, or wire up an `AbortController` to actually stop the work.
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:

JS
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 receives undefined, 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 or await instead.

  • Running sequentially when you could parallelize — independent work belongs in Promise.all.

  • Mixing await and .then() on the same promise — pick one style per operation for clarity.

Next
Promises read top-to-bottom but still have ceremony. [async / await](/nodejs/async-await-refresher) makes them look like ordinary synchronous code.