Topic 10.5
Promises
In one line
A promise is an object standing for a result that isn't ready yet: it starts pending and settles once, as fulfilled with a value or rejected with a reason, and you react with then, catch and finally.
Think of it like this
The buzzer at a food court. You order a pizza and get a buzzer. It isn't the pizza, but it stands for the pizza you'll get. Right now it's waiting (pending). Later it buzzes either "your food is ready" (fulfilled, with the pizza) or "sorry, we're out" (rejected, with a reason). It buzzes once, and it will never change its mind afterwards. Meanwhile you can find a table.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Promise
- An object that stands for a result that will be ready later.
- Pending
- The promise hasn't finished yet.
- Fulfilled
- The promise finished successfully and has a value.
- Rejected
- The promise failed and has a reason, usually an
Error. - Settled
- Finished, either fulfilled or rejected. A settled promise never changes again.
- Executor
- The function you pass to
new Promise; it receivesresolveandrejectand runs straight away. - resolve / reject
- The two functions an executor calls to fulfil the promise with a value or reject it with an error.
- then / catch / finally
- Methods that register what to do when a promise fulfils, rejects, or settles either way.
Step by step
01A promise starts pending
new Promise runs the executor at once. Here it starts a timer and returns. The promise is pending, and the code after it carries on.
const pizza = new Promise((resolve, reject) => {
setTimeout(() => resolve("Margherita"), 1000);
});
console.log("Order placed"); // runs immediately02It settles once
When the timer fires, resolve("Margherita") fulfils the promise. Any later resolve or reject call is ignored.
03React with then, catch and finally
then handles the value, catch the error and finally runs in both cases. Each callback runs later, as a microtask.
pizza
.then((name) => console.log("Eating a", name))
.catch((err) => console.log("No pizza:", err.message))
.finally(() => console.log("Clear the table"));04Wrap a callback API
To give setTimeout a promise interface, create a promise whose executor starts the timer and passes resolve as its callback. This tiny wait helper appears throughout this phase.
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
wait(500).then(() => console.log("Half a second later"));05Reject with real errors
Call reject(new Error("Out of dough")), not reject("Out of dough"). An Error has a message, a name and a stack trace showing where it came from, which every catch and error tracker expects.
Try it yourself
- 1
Reject instead
In the first example, change
resolve("Margherita")toreject(new Error("Burnt"))and add.catch((e) => console.log("4. Rejected:", e.message)). Predict the four lines. - 2
Swap the order of reject and resolve
In the third example, put the
reject(...)line first infirst. Predict what prints forfirstnow. Is anything logged as uncaught? - 3
Add a wait helper
Write
function wait(ms) { return new Promise((r) => setTimeout(r, ms)); }and usewait(200).then(() => console.log("done")). Predict wheredoneappears relative to aconsole.logafter it.
Code & diagrams
The executor is synchronous; only the `resolve` inside the timer happens later.
Preview
Console
Nothing logged yet.Expected console output
1. Before new Promise
2. The executor runs straight away
3. After new Promise: still pending
4. Fulfilled with MargheritaOrder 1 skips `catch`; order 2 skips `then`. `finally` runs for both.
Preview
Console
Nothing logged yet.Expected console output
Order 1: eating a Margherita
Order 1: clear the table
Order 2 problem: Out of dough
Order 2: clear the tableAll three promises were settled before the last line ran, yet their callbacks still waited until the synchronous code finished.
Preview
Console
Nothing logged yet.Expected console output
Synchronous code ends here
Value: first value
Caught: thrown inside the executor
Already fulfilled, still async: 42`withResolvers` returns the promise together with its `resolve` and `reject`, handy when the code that settles it lives somewhere else, such as an event listener.
Preview
Console
Nothing logged yet.Expected console output
User agreedBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to call resolve
Write new Promise((resolve) => { setTimeout(() => console.log("timer done"), 100); }).then(() => console.log("finished"));.
Break #2
Call new Promise without an executor
Write const p = new Promise();.
Break #3
Reject with a string
Write reject("failed") and later catch((err) => console.log(err.message)).
Myth vs fact
Myth
new Promise makes the code inside it asynchronous.
Fact
The executor runs synchronously, immediately. Only work it starts (a timer, a request) is async, and the then callbacks run later.
Myth
If a promise is already resolved, then runs its callback immediately.
Fact
The callback is queued as a microtask and runs after the current synchronous code finishes, every time.
Myth
A promise is the async operation itself, and cancelling the promise stops the work.
Fact
A promise is only a placeholder for the result. There is no cancel(). To stop work such as fetch, use an AbortController (Phase 11).
Myth
"Resolved" means "fulfilled".
Fact
A promise resolved with another promise is locked to that promise but may still be pending, and may even end up rejected. "Fulfilled" is the precise word for success.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Promises follow thenables: any object with a
thenmethod.Promise.resolve(obj)andresolve(obj)callobj.then(...)to adopt its state. This lets libraries interoperate, but it means resolving with an object that happens to have athenmethod behaves surprisingly. - ▸
Promise.resolve(p)returnspitself whenpis already a native promise of the same constructor, so it's cheap to normalise values that might or might not be promises. - ▸
finallydoesn't receive the value and doesn't change it: the original value or reason passes through. But if thefinallycallback throws or returns a rejected promise, that new error replaces the original outcome. - ▸
In DevTools, logging a promise shows
Promise {<pending>}or its state and value ([[PromiseState]],[[PromiseResult]]), which you can't read from code. Hectal's one-line console always printsPromise { <pending> }, so log the value insidetheninstead.Promise.try(fn), newer than the editions covered here, wraps a function that may throw or return a promise.
Remember this
- 1
A promise is an object representing the eventual result of an async operation. Functions like
fetchreturn one immediately, before the work is done. A promise is always in one of three states: pending (not finished), fulfilled (finished with a value) or rejected (failed with a reason, usually anError). Fulfilled and rejected together are called settled. Once settled, a promise never changes state or value again. - 2
You react to a promise by registering callbacks.
promise.then(onFulfilled)runs when it fulfils, receiving the value.promise.catch(onRejected)runs when it rejects, receiving the reason.promise.finally(onFinally)runs either way, with no argument; use it for clean-up such as hiding a spinner. You can callthenbefore or after the promise settles: if it's already settled, your callback still runs, just as soon as possible. - 3
These callbacks always run asynchronously, as microtasks (Topic 10.3), even if the promise is already settled. So code after
.then(...)always runs before the callback. That consistency is one of the fixes for the callback problems of Topic 10.4. - 4
You create a promise with
new Promise(executor). The executor is a function(resolve, reject) => { ... }that the constructor calls immediately and synchronously. Inside it, start your async work and callresolve(value)when it succeeds orreject(error)when it fails. Only the first call counts; later calls are ignored. If the executor throws, the promise is rejected with that error. - 5
Shortcuts:
Promise.resolve(value)makes an already-fulfilled promise (or returnsvalueunchanged if it's already a promise), andPromise.reject(error)makes an already-rejected one. Always reject with anErrorobject, not a string, so you keep a message and a stack trace. Resolving with another promise doesn't fulfil with the promise object: your promise follows that one and settles the same way. - 6
Mostly you use promises that other APIs give you (
fetch,navigator.clipboard.readText(),response.json()). You create them yourself mainly to wrap older callback or event APIs, like turningsetTimeoutintowait(ms). Promises arrived in ES2015;.finallywas added in ES2018.
Explain it without notes
What are the three states of a promise, and what does "settled" mean?
When does the executor passed to new Promise run, and what happens if it throws?
What's the difference between then, catch and finally?
Why do promise callbacks run asynchronously even when the promise is already fulfilled?
Practice
Write wait(ms, value) that returns a promise fulfilled with value after ms. Use it to log ready after 100 ms.
Write checkAge(age) that returns a promise rejected with Error("Too young") if age < 18 and fulfilled with "Welcome" otherwise. Call it with 20 and 12, logging both outcomes, and log checked after each with finally.
Trade-offs
- ↔
Promises give one-time settling, async-only callbacks and error propagation, at the cost of a small allocation per step; for repeating events, plain callbacks or async iterators fit better.
- ↔
Wrapping a callback API in
new Promisegives a modern interface but adds a layer; when the API already offers a promise version (most modern ones do), use that instead.
Done when you can
I can name the three promise states and explain settled.
I can create a promise with
new Promise,resolveandreject.I can handle results with
then,catchandfinally.I know the executor runs synchronously and callbacks run as microtasks.
I always reject with an
Errorobject.