Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ES2015+ ~20 min· topic 5 of 8

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 receives resolve and reject and 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.

pizza.jswhole filejavascript
const pizza = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Margherita"), 1000);
});
console.log("Order placed"); // runs immediately

02It settles once

When the timer fires, resolve("Margherita") fulfils the promise. Any later resolve or reject call is ignored.

It settles oncediagram
Rendering diagram…

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.

react.jswhole filejavascript
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.

wait.jswhole filejavascript
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. 1

    Reject instead

    In the first example, change resolve("Margherita") to reject(new Error("Burnt")) and add .catch((e) => console.log("4. Rejected:", e.message)). Predict the four lines.

  2. 2

    Swap the order of reject and resolve

    In the third example, put the reject(...) line first in first. Predict what prints for first now. Is anything logged as uncaught?

  3. 3

    Add a wait helper

    Write function wait(ms) { return new Promise((r) => setTimeout(r, ms)); } and use wait(200).then(() => console.log("done")). Predict where done appears relative to a console.log after it.

Code & diagrams

Creating a promise: the executor runs immediately New tab

The executor is synchronous; only the `resolve` inside the timer happens later.

Preview

Press Run to see the page.

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 Margherita
Fulfilled or rejected: then, catch and finally New tab

Order 1 skips `catch`; order 2 skips `then`. `finally` runs for both.

Preview

Press Run to see the page.

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 table
Settles once, throws reject, callbacks are always async New tab

All three promises were settled before the last line ran, yet their callbacks still waited until the synchronous code finished.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Synchronous code ends here
Value: first value
Caught: thrown inside the executor
Already fulfilled, still async: 42
Promise.withResolvers: resolve from outside ES2024+ New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

User agreed

Break 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"));.

terminal
$ Console
── what you'll see ──
timer done (and "finished" never appears)

Break #2

Call new Promise without an executor

Write const p = new Promise();.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Promise resolver undefined is not a function

Break #3

Reject with a string

Write reject("failed") and later catch((err) => console.log(err.message)).

terminal
$ Console
── what you'll see ──
undefined

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 then method. Promise.resolve(obj) and resolve(obj) call obj.then(...) to adopt its state. This lets libraries interoperate, but it means resolving with an object that happens to have a then method behaves surprisingly.

  • ▸

    Promise.resolve(p) returns p itself when p is already a native promise of the same constructor, so it's cheap to normalise values that might or might not be promises.

  • ▸

    finally doesn't receive the value and doesn't change it: the original value or reason passes through. But if the finally callback 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 prints Promise { <pending> }, so log the value inside then instead. Promise.try(fn), newer than the editions covered here, wraps a function that may throw or return a promise.

Remember this

  1. 1

    A promise is an object representing the eventual result of an async operation. Functions like fetch return 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 an Error). Fulfilled and rejected together are called settled. Once settled, a promise never changes state or value again.

  2. 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 call then before or after the promise settles: if it's already settled, your callback still runs, just as soon as possible.

  3. 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. 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 call resolve(value) when it succeeds or reject(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. 5

    Shortcuts: Promise.resolve(value) makes an already-fulfilled promise (or returns value unchanged if it's already a promise), and Promise.reject(error) makes an already-rejected one. Always reject with an Error object, 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. 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 turning setTimeout into wait(ms). Promises arrived in ES2015; .finally was added in ES2018.

Explain it without notes

01

What are the three states of a promise, and what does "settled" mean?

02

When does the executor passed to new Promise run, and what happens if it throws?

03

What's the difference between then, catch and finally?

04

Why do promise callbacks run asynchronously even when the promise is already fulfilled?

Practice

01

Write wait(ms, value) that returns a promise fulfilled with value after ms. Use it to log ready after 100 ms.

02

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 Promise gives 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, resolve and reject.

  • I can handle results with then, catch and finally.

  • I know the executor runs synchronously and callbacks run as microtasks.

  • I always reject with an Error object.