Promises

Work with values that arrive later, and combine several of them.

21of 32
≈ 12Min read
8Parts

A promise is a value that is not here yet

It has three states: pending, then either fulfilled with a value or rejected with a reason. It settles once and never changes again.

making one
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const load = (id) => new Promise((resolve, reject) => {
  const img = new Image();
  img.onload = () => resolve(img);
  img.onerror = () => reject(new Error(`Could not load ${id}`));
  img.src = `/media/${id}.jpg`;
});
Only wrap things that are not already promises

If a function already returns a promise, new Promise((res) => fn().then(res)) adds nothing but a place to lose errors. Return the promise.

then, catch, finally

  • Whatever a then returns becomes the next then's input.
  • Returning a promise from a then waits for it — chains do not nest.
  • A catch handles every rejection above it, not below.
  • finally does not receive the value and does not change it.
and the shape of a chain
load(7)
  .then((img) => resize(img))        // returning a promise flattens it
  .then((small) => show(small))
  .catch((err) => showError(err))    // catches anything above it
  .finally(() => spinner.remove());  // runs either way, passes the value on

Errors fall through

which is the point
step1()
  .then(step2)      // if step1 rejects, this is skipped
  .then(step3)      // and this
  .catch(handle);   // and it lands here

// a throw inside a then becomes a rejection
.then(() => { throw new Error("no"); })
.catch((e) => e.message)      // "no"
A promise with no catch is an unhandled rejection

It does not crash the page, but it logs an error you will not see in production. Attach a catch, or await it inside a try.

Combining

when to use each
// I need all of them, and one failure means the whole thing failed
const [user, doors] = await Promise.all([getUser(), getDoors()]);

// I want whatever I can get
const results = await Promise.allSettled(ids.map(load));
const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);

// a timeout
const data = await Promise.race([fetchJson(url), timeout(5000)]);

// several mirrors; the first that works
const data = await Promise.any(mirrors.map(fetchJson));
CombinatorResolves whenRejects when
Promise.allEvery one fulfils — an array of valuesAny one rejects, immediately
Promise.allSettledEvery one settles — {status, value|reason}Never
Promise.raceThe first one settles, either wayIf the first to settle rejected
Promise.anyThe first one fulfilsOnly if all of them reject

Sequential or parallel

Parallel is not always right: a hundred simultaneous requests can be worse than four at a time, and an API may rate-limit you.

the difference is where you await
// sequential — each waits for the last
for (const id of ids) {
  await save(id);
}

// parallel — all start at once
await Promise.all(ids.map((id) => save(id)));

// parallel, but at most 4 at a time
async function pool(items, limit, fn) {
  const running = new Set();
  const out = [];
  for (const item of items) {
    const p = fn(item).finally(() => running.delete(p));
    running.add(p);
    out.push(p);
    if (running.size >= limit) await Promise.race(running);
  }
  return Promise.all(out);
}

Starting versus awaiting

a promise begins when it is created
// both requests are already in flight here
const a = getUser();
const b = getDoors();
const user = await a;
const doors = await b;      // it has been loading all along

// this is the slow version
const user = await getUser();
const doors = await getDoors();   // only starts now

Turning a callback API into a promise

the one legitimate use of the constructor
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const readFile = (file) => new Promise((resolve, reject) => {
  const fr = new FileReader();
  fr.onload = () => resolve(fr.result);
  fr.onerror = () => reject(fr.error);
  fr.readAsText(file);
});

Rules worth keeping

  • Always return the promise from a function, so the caller can wait for it.
  • Never leave a promise without a catch or an enclosing try.
  • Reject with an Error, not a string — only an Error carries a stack.
  • Promise.all fails fast: if you want every result regardless, use allSettled.
  • A promise is not lazy. Creating it starts the work.

Practice