Course outline · 0% complete

0/27 lessons0%

Course overview →

From callbacks to promises

lesson 5-1 · ~11 min · 11/27

The .then callback runs first, because microtasks drain before the next macrotask.

Promise callbacks are microtasks, and the whole microtask queue drains before any macrotask gets a turn.

Keep that model handy, because everything in this unit runs through those queues. A promise is not a new kind of concurrency, it is a nicer way to schedule callbacks onto the queue you already know.

The single-threaded rule from lesson 4-1 still holds too. A promise never runs two callbacks at once, and it never interrupts code that is already on the stack.

What promises add is structure. The mechanism underneath is the same event loop, and the difference is entirely in how the code reads.

Why promises exist

Async work such as network calls, timers, and file reads finishes later.

The old pattern was to pass a callback, meaning a function that says call this when you are done. Nest a few of those and you get the famous callback pyramid, where step 3 lives four indents deep inside step 2 inside step 1.

Depth is not the only problem. Each level needs its own error check, there is no way to return a value out of the nest, and one callback that fires twice corrupts everything downstream.

A promise flips it around. It is an object that represents a future value, in one of three states.

StateMeaning
pendingthe work is still running
fulfilleddone, with a value
rejectedfailed, with a reason

A promise settles exactly once, and then never changes again. You attach what happens next with .then(callback) instead of nesting.

You create one with new Promise(executor).

The executor is the name for the function you hand to the constructor. JavaScript calls it immediately and passes it two functions, resolve(value) to fulfill and reject(reason) to fail.

Your async work runs inside the executor and reports back through those two calls, which is the bridge from any callback-based API into promise land.

Wrapping a timer in a promise

The script keeps running while the promise is pending.

const p = new Promise((resolve) => {
  setTimeout(() => resolve("data loaded"), 50);
});

console.log("request sent");

p.then((value) => {
  console.log("got: " + value);
});

console.log("doing other work");

Output

request sent
doing other work
got: data loaded

new Promise takes a function that receives resolve. The executor starts a timer, the script keeps going, and .then fires when resolve is called 50 ms later.

The value passed to resolve is the value .then receives, which is the whole data path. Nothing is returned from the executor.

p is a real value you can pass around, store in an array, or return from a function. That is the structural difference from a callback, which is a one-way hand-off.

Attaching a second .then to p works fine and both callbacks run. Attaching one after the promise has already settled also works, and the callback is queued as a microtask immediately.

The executor here ignores its second parameter. A real one would call reject(new Error(...)) on failure, which is the next lesson's subject.

It runs immediately and synchronously, during the new Promise(...) call itself.

The constructor calls the executor right away, before new Promise even returns.

In the example it ran before "request sent" printed, and its only job happened to be starting a timer, so nothing looked synchronous from the outside.

What is deferred to the microtask queue are the .then callbacks. Starting the work is synchronous, and reacting to its result is not.

That distinction has a practical consequence. Building a promise begins the work, so const p = fetchUser() fires the request immediately whether or not anyone ever calls .then on it.

If you want work that starts only on demand, wrap it in a function and call the function later. A promise is a running operation rather than a recipe for one.

A synchronous throw inside the executor is caught by the promise machinery and turns into a rejection, which is a small mercy worth knowing.

pendingfulfilled (value)rejected (reason)resolve(value)reject(reason)
A promise settles once: pending moves to fulfilled or rejected, and the arrows never point back.

The value stays "done", because a settled promise never changes.

Settling is permanent. The first resolve wins, and every later resolve or reject call is silently ignored.

The silence is deliberate rather than sloppy. A second call is not an error, it simply has no effect, so racing code cannot corrupt a settled promise.

This one-shot guarantee is what makes promises safe to hand around your program. Any number of consumers can attach .then callbacks, and every one of them sees the same single outcome.

Compare that to a raw callback, which offers no such protection. A buggy library that invokes your callback twice runs your handler twice, and finding that bug is genuinely unpleasant.

The same guarantee is why a promise cannot represent a stream of values. One promise means one result, and repeated events need an event emitter or an async iterator instead, which unit 7 touches on.

delay

A reusable promise-returning sleep.

function delay(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

console.log("start");
delay(30, "done").then((v) => console.log(v));

Output

start
done

The shape copies the previous example exactly, returning new Promise and resolving inside a setTimeout.

ms and value are both closed over by the arrow inside the executor, which is lesson 1-2's mechanism doing quiet work here.

You will reuse this exact helper for the rest of the course, since it is the simplest way to simulate slow work without a network.

delay is the promise-shaped answer to the busy-wait from lesson 4-1. It yields the stack instead of holding it, so timers and clicks keep working during the pause.

With await from unit 6 it reads as a real sleep, since await delay(30) pauses one function and blocks nothing else.

Making value optional is a small improvement worth noting. Called as delay(30), it resolves with undefined, which is fine when you only want the pause.