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.
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
thenreturns becomes the nextthen's input. - Returning a promise from a
thenwaits for it — chains do not nest. - A
catchhandles every rejection above it, not below. finallydoes not receive the value and does not change it.
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
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
// 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));
| Combinator | Resolves when | Rejects when |
|---|---|---|
Promise.all | Every one fulfils — an array of values | Any one rejects, immediately |
Promise.allSettled | Every one settles — {status, value|reason} | Never |
Promise.race | The first one settles, either way | If the first to settle rejected |
Promise.any | The first one fulfils | Only 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.
// 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
// 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
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
catchor an enclosingtry. - Reject with an
Error, not a string — only an Error carries a stack. Promise.allfails fast: if you want every result regardless, useallSettled.- A promise is not lazy. Creating it starts the work.