Skip to content
Topic 18 of 21
Theory

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" and reject(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

  1. A forgotten return in then. .then((x) => { x * 2; }) passes undefined on. Use either a short arrow (x) => x * 2 or return.
  2. A promise inside then without return. The chain won't wait for it, and nobody will catch its errors.
  3. No catch. A rejected promise without a handler is an "unhandled error"; in Node.js it stops the program.
  4. Waiting one by one for what could run in parallel. Start independent loads together and wait with Promise.all.
  5. Trying to get the value synchronously. const x = delay(100); — x holds a promise, not the result. The result is available only in then (or with await, 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

Pause

Easy+10 XP

Write a function delay(ms, value) that returns a promise. After ms milliseconds the promise is fulfilled with the value value.

delay(500, "ready").then((v) => console.log(v));   // after half a second: ready

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.