Promises
A promise is an object standing in for a value that has not arrived yet. That
one idea fixes every complaint from the last lesson: sequence becomes a list
rather than a staircase, errors are handled once, and try/catch starts
working again.
What it is
const orderPromise = getOrder(1);
console.log(orderPromise);
Promise { <pending> }
Not the order — a receipt for the order. It is in one of three states:
| State | Means |
|---|---|
| pending | Still waiting |
| fulfilled | Done, with a value |
| rejected | Failed, with a reason |
A promise settles once and never changes again. That is the guarantee callbacks could not give: it cannot fulfil twice, and it cannot fulfil and then reject.
Using one
getOrder(1)
.then((order) => {
console.log(order.customer);
})
.catch((error) => {
console.log('Failed:', error.message);
});
.then runs on success with the value; .catch runs on failure with the reason.
Compare with the error-first callback — the two paths are now separate, and
the success path contains no error checking at all.
.finally runs either way, and is where the loading spinner gets turned off:
getOrder(1)
.then(showOrder)
.catch(showError)
.finally(() => {
spinner.hidden = true;
});
Chaining, which is the point
.then returns a new promise, so they queue up:
Promise.resolve(2)
.then((n) => n * 10)
.then((n) => n + 1)
.then((n) => console.log(n));
21
Each .then receives what the previous one returned. That is the pyramid,
flattened into a list:
getCustomer(1)
.then((customer) => getOrder(customer.id))
.then((order) => getSlot(order.id))
.then((slot) => console.log(slot.time))
.catch(showError);
Four steps, no nesting, and one .catch for all of them — a rejection
anywhere skips every remaining .then and lands there. Compare the three
identical error blocks from the last lesson.
Returning a promise from a .then flattens it. getOrder(customer.id)
returns a promise, and the next .then receives the order rather than the
promise. Without that, chaining would nest as badly as callbacks.
The trap: the missing return
Promise.resolve(2)
.then((n) => {
n * 10;
})
.then((n) => console.log(n));
undefined
Braces make a function body, so nothing is returned, so the next .then gets
undefined — module 3's arrow-function trap, in the place it costs most.
It is worse with an asynchronous step:
getCustomer(1)
.then((customer) => {
getOrder(customer.id);
})
.then((order) => console.log(order));
undefined
The order is fetched — but nothing was returned, so the chain did not wait for
it and moved on immediately. A .then that starts async work without
returning it breaks the sequence silently, and any error inside it escapes your
.catch entirely.
Rule: every .then either returns a value or returns a promise.
Recovering
.catch also returns a promise, so a chain continues after it:
Promise.reject(new Error('x'))
.catch(() => 'recovered')
.then((value) => console.log(value + '!'));
recovered!
Useful for a default when a request fails. It also means a .catch in the middle
of a chain handles only what came before it — put it last unless you
specifically want to recover and carry on.
Making one
Usually you do not; fetch and other APIs hand you promises already. You write
one to wrap a callback-style API:
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
wait(1000).then(() => console.log('one second later'));
new Promise takes a function with two arguments: call resolve(value) to
fulfil, reject(error) to fail.
Wrapping an error-first callback is the common case:
function getOrder(id) {
return new Promise((resolve, reject) => {
getOrderCallback(id, (error, order) => {
if (error) {
reject(error);
return;
}
resolve(order);
});
});
}
Written once, and the whole API is modern.
Always reject with an Error, not a string — module 5's rule. You want a
stack trace.
Two shortcuts: Promise.resolve(value) and Promise.reject(error) make
already-settled promises, useful for tests and for a cached value.
Several at once
This is where promises stop being tidier callbacks and start being genuinely more capable. The counter-and-flag bookkeeping from the last lesson becomes:
const orders = await Promise.all([getOrder(1), getOrder(2), getOrder(3)]);
| Combinator | Resolves when | Rejects when |
|---|---|---|
Promise.all |
All succeed — array of results, in order | Any fails, immediately |
Promise.allSettled |
All settle — never rejects | Never |
Promise.race |
The first settles, either way | If the first settles as a rejection |
Promise.any |
The first success | Only if all fail |
Promise.all gives results in the order you passed them in, regardless of
which finished first — the index bookkeeping, done for you.
Promise.all fails fast, and that is often wrong. Loading three independent
panels, one failing should not blank all three:
const results = await Promise.allSettled([getOrders(), getMenu(), getSlots()]);
for (const result of results) {
if (result.status === 'fulfilled') {
render(result.value);
} else {
showError(result.reason);
}
}
fulfilled,rejected
allSettled gives { status, value } or { status, reason } for each, and
never rejects. Use all when you need everything and allSettled when you
want as much as you can get.
race is for timeouts:
Promise.race([
getOrder(1),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Too slow')), 5000),
),
]);
fetch has no timeout of its own, and on a train that matters.
They start immediately
A common misunderstanding:
const promise = getOrder(1);
The request is already in flight. A promise is not a recipe you run later —
the work started when you called the function. .then only decides what happens
with the result.
That is why Promise.all([a(), b()]) runs both at once: both were called, and
therefore both started, before all ever saw them.
Check your work
A promise is a receipt for a future value, in one of three states, and it settles once and stays settled.
.then returns a new promise, so chains flatten the callback pyramid into a
list, and one .catch at the end handles a rejection from any step.
Returning a promise from .then flattens it, so the next step gets the
value.
A .then with braces and no return passes undefined on. If it started
async work without returning it, the chain does not wait and errors inside escape
your .catch.
.catch returns a promise too, so .catch(() => 'recovered').then(...)
continues with the recovery value.
new Promise((resolve, reject) => ...) wraps a callback API. Reject with an
Error.
Promise.all returns results in argument order and fails fast;
allSettled never rejects and reports { status, value } or
{ status, reason } per entry. race settles on the first to finish;
any waits for the first success.
Promises start immediately. The work began when the function was called, not
when .then was attached.
Practice
- Log a promise before it settles and confirm you see
Promise { <pending> }. - Write
wait(ms)returning a promise and use it with.then. - Chain three
.thens doing arithmetic and predict the output. - Write the missing-return bug. Use braces in a
.thenwithoutreturnand watchundefinedarrive downstream. Fix it twice — by removing the braces and by addingreturn. - Make the async version of that bug: call another promise-returning function
inside a
.thenwithout returning it, and confirm the chain does not wait. - Build a three-step chain with one
.catchat the end. Make the middle step reject and confirm the third step is skipped. - Put a
.catchin the middle instead, returning a default, and confirm the chain continues. - Add
.finallyand confirm it runs on both paths. - Wrap an error-first callback function in a promise and use it with
.then/.catch. - Use
Promise.allon three waits of different lengths. Time it, and confirm the results come back in argument order rather than finishing order. - Make one of them reject and watch
allfail immediately. Then switch toallSettledand handle each result. - Harder. Write
withTimeout(promise, ms)usingPromise.racethat rejects with a clearErrorif the promise takes too long. Test it against a fast and a slow operation. Then answer, in a comment: when the timeout wins, what happens to the original request?
Next: async/await — the same promises, written as if they were synchronous.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship