Promises
Why it matters
In the previous topic the asynchronous result was passed through a callback, and with several steps you got a
"staircase" of nested functions. A promise is an object that represents a future result. You can return
it from a function with an ordinary return, store it in a variable, pass it on and subscribe to the result.
All of modern JavaScript is built on promises: fetch (requests to a server), reading files in Node.js,
async/await, loading data in React. Even if you write await, there are always promises underneath — so you
need to understand them.
Three states
A promise is always in one of three states:
| State | What it means |
|---|---|
pending |
there is no result yet |
fulfilled |
there is a result — a value |
rejected |
an error happened — there is a reason |
From pending a promise moves to one of the two final states once and never changes again.
Creating a promise: new Promise
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve("done"), ms);
});
}
console.log("Waiting...");
delay(100).then((result) => console.log(result));
Output:
Waiting...
done
- You pass an "executor" function to
new Promise(…). It runs immediately and receives two functions:resolve(value)— "succeeded" andreject(error)— "failed". .then(callback)— "when there's a result, call the callback with it".
An example with an error:
function checkAge(age) {
return new Promise((resolve, reject) => {
if (age >= 18) resolve("Access granted");
else reject(new Error("Too early"));
});
}
checkAge(20).then((text) => console.log(text));
checkAge(12)
.then((text) => console.log(text))
.catch((err) => console.log("Error: " + err.message));
Output:
Access granted
Error: Too early
.catch(callback) catches a rejection. It's customary to pass an Error object to reject, just like with throw.
finally
.finally(callback) runs in any case — success or not. For example, to hide a loading indicator:
checkAge(12)
.then((text) => console.log(text))
.catch((err) => console.log("Error: " + err.message))
.finally(() => console.log("Loading finished"));
then chains
The main power of promises: then itself returns a new promise. Whatever the callback returned becomes the
value of the next promise. So steps follow one another without nesting:
Promise.resolve(2)
.then((x) => x * 10) // 20
.then((x) => x + 1) // 21
.then((x) => console.log("Result: " + x));
Output:
Result: 21
If the callback returns a promise, the chain waits for it:
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
wait(50, "Ann")
.then((name) => wait(50, name + " from Dushanbe")) // wait for one more promise
.then((text) => console.log(text));
Output:
Ann from Dushanbe
Compare with the "callback hell" from the previous topic: the steps are written as a flat list.
Errors in a chain
An error (via reject or an ordinary throw inside then) skips all the following thens and lands in the
nearest catch. So one catch at the end is enough for the whole chain:
Promise.resolve(1)
.then((x) => {
throw new Error("broke at step 2");
})
.then((x) => console.log("this step is skipped"))
.catch((err) => console.log("Caught: " + err.message))
.then(() => console.log("The chain continues after catch"));
Output:
Caught: broke at step 2
The chain continues after catch
catch also returns a promise: what it returned becomes the new value. That's how you substitute a default
value: .catch(() => []).
Several promises at once
You often need to start several tasks in parallel and wait for all of them. Promise has static methods for that:
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const start = Date.now();
Promise.all([wait(100, "A"), wait(100, "B"), wait(100, "C")]).then((values) => {
console.log(values);
console.log("Faster than 200 ms: " + (Date.now() - start < 200));
});
Output:
["A","B","C"]
Faster than 200 ms: true
Three waits of 100 ms took about 100 ms, not 300: they ran at the same time.
| Method | Settles when | Result |
|---|---|---|
Promise.all(arr) |
all fulfilled | array of values in array order; on the first error — rejected |
Promise.allSettled(arr) |
all finished, any way | array of { status, value } or { status, reason } |
Promise.race(arr) |
the first one finished | its value or error |
Promise.any(arr) |
the first successful one | its value |
allSettled is handy when one failure shouldn't break everything:
Promise.allSettled([Promise.resolve(1), Promise.reject(new Error("no")), Promise.resolve(3)]).then((results) => {
console.log(results.map((r) => (r.status === "fulfilled" ? r.value : "error")));
});
Output:
[1,"error",3]
And race is a way to set a time limit: "if the answer didn't come within 3 seconds — error".
How it works inside: microtasks
The then, catch and finally callbacks are never called right away, even if the promise is already
fulfilled. They go into a special queue — microtasks. The event loop handles it right after the current
code and before ordinary tasks like timers:
console.log("1: synchronous code");
setTimeout(() => console.log("4: timer"), 0);
Promise.resolve().then(() => console.log("3: then"));
console.log("2: synchronous code");
Output:
1: synchronous code
2: synchronous code
3: then
4: timer
The order is: all synchronous code → all microtasks (promises) → the next task from the queue (timer, click) → microtasks again → …
One more important property: the executor function in new Promise runs synchronously, right at creation.
Only the reactions to the result are asynchronous.
Common mistakes
- A forgotten
returninthen..then((x) => { x * 2; })passesundefinedon. Use either a short arrow(x) => x * 2orreturn. - A promise inside
thenwithoutreturn. The chain won't wait for it, and nobody will catch its errors. - No
catch. A rejected promise without a handler is an "unhandled error"; in Node.js it stops the program. - Waiting one by one for what could run in parallel. Start independent loads together and wait with
Promise.all. - Trying to get the value synchronously.
const x = delay(100);—xholds a promise, not the result. The result is available only inthen(or withawait, the next topic).
Cheat sheet
| What | How |
|---|---|
| Create | new Promise((resolve, reject) => { ... }) |
| Ready-made promise | Promise.resolve(x), Promise.reject(err) |
| Result | .then((value) => ...) |
| Error | .catch((err) => ...) |
| In any case | .finally(() => ...) |
| Wait for all | Promise.all([...]) |
| All, even with errors | Promise.allSettled([...]) |
| Who's first | Promise.race([...]) |
| Order | synchronous code → microtasks (then) → timers |