LoopLens

How JavaScript promises work

A promise is an object that stands for a value you will get later, or an error. It starts pending and settles exactly once, either fulfilled with a value or rejected with a reason. You react to it with .then, .catch and .finally, and those callbacks always run later, as microtasks.

Try it

This chain passes a value from one .then to the next. Press Next step and notice that sync prints first, although it is the last line.

Test yourself. When this is on, LoopLens pauses before every step that prints to the console and asks what it will print. Pick an answer to check it, or choose Just show me.Step 0 of 14 · virtual clock 0 ms
Event loop:Script (now)MicrotasksTask, then it repeats.The event loop runs the whole script first. Then it runs every microtask, then one task, and repeats.

Code (current line highlighted)

1current line: Promise.resolve(1)
2 .then(x => {
3 console.log("first", x);
4 return x + 1;
5 })
6 .then(x => console.log("second", x));
7
8console.log("sync");

Call stack (top first)The function running right now is on top. Calling a function adds a frame; returning removes it. Only one frame runs at a time.

main script

Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.

empty
The whole script runs as ONE task on the call stack. Lines run top to bottom.

Microtask queue (next first)Promise callbacks (.then, code after await, queueMicrotask) wait here. Once the call stack is empty, all of them run before the next task.

empty

Task queue (next first)Timer callbacks wait here. The event loop takes one task, runs it, then empties the microtask queue before taking the next.

empty

Console output

nothing yet

Scope and variables (yellow = changed this step)

no variables yet

Timeline (click a block or use the arrow keys)Script, function calls and returnsawait and microtasksTimers and tasksConsole outputErrors

Step by step: what the example does

Promise.resolve(1)
  .then(x => {
    console.log("first", x);
    return x + 1;
  })
  .then(x => console.log("second", x));

console.log("sync");
  1. Promise.resolve(1) creates a promise that is already fulfilled with 1.
  2. The first .then callback joins the microtask queue right away, because its promise is settled. It does not run yet. Each .then also returns a new promise, which the second .then waits for.
  3. console.log("sync") runs and the script finishes.
  4. The first callback runs, prints first 1 and returns 2. That fulfills the promise returned by the first .then, so the second callback joins the microtask queue.
  5. The second callback runs and prints second 2.

Creating a promise

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

The function passed to new Promise (the executor) runs immediately and synchronously. It starts the work, here a timer, and calls resolve(value) or reject(reason) when the work is done. Calling either one a second time does nothing: a promise settles only once. The Sleep with a promise example in the menu above uses this function with await.

Chaining with then

  • Every .then returns a new promise, so calls can be chained.
  • Whatever a callback returns becomes the value of that new promise and is passed to the next .then.
  • If a callback returns another promise, the chain waits for that promise to settle before moving on.
  • If a callback throws, the new promise is rejected, and the chain jumps to the next .catch.

Handling errors with catch

Promise.reject("oops")
  .then(() => console.log("skipped"))
  .catch(e => console.log("caught", e))
  .then(() => console.log("after catch"));

console.log("sync");

This prints sync, caught oops, after catch. A rejection skips every .then success callback until it reaches a .catch. After .catch handles the error, the chain continues as fulfilled, so the last .then runs. A rejection that no .catch handles is reported as Uncaught (in promise).

finally: run cleanup either way

function load(ok) {
  return ok ? Promise.resolve("data") : Promise.reject("network error");
}

load(true)
  .then(v => console.log("got", v))
  .finally(() => console.log("hide spinner (1)"));

load(false)
  .catch(e => console.log("failed:", e))
  .finally(() => console.log("hide spinner (2)"));

.finally runs whether the promise was fulfilled or rejected, which makes it the place to hide a loading spinner or close a connection. Its callback receives no value, and the original result passes through to the next step unchanged (unless the callback itself throws).

Promise.all vs Promise.race

const slow = new Promise(resolve => setTimeout(() => resolve("slow"), 100));
const fast = new Promise(resolve => setTimeout(() => resolve("fast"), 50));

Promise.race([slow, fast]).then(v => console.log("race:", v));
Promise.all([slow, fast]).then(v => console.log("all:", v.join(", ")));
console.log("waiting...");

This prints waiting..., then race: fast after 50 ms, then all: slow, fast after 100 ms. Promise.race settles with whichever promise settles first. Promise.all waits for all of them and keeps the results in the order you passed them in, not the order they finished. If any of them is rejected, Promise.all is rejected straight away.

Promises and async/await

An async function always returns a promise, and await pauses the function until a promise settles. The rest of the function then continues as a microtask, just like a .then callback. Choose async/await order in the example menu to see this. Which callbacks run first, and why, is covered in microtasks vs macrotasks.

Common mistakes

  • Forgetting to return inside .then. The next step then receives undefined and does not wait for any work started in the callback.
  • Nesting instead of chaining. Return the inner promise and add the next .then to the outer chain.
  • Thinking the executor runs later. The code inside new Promise(...) runs immediately; only the callbacks are delayed.
  • Leaving rejections unhandled. End chains with a .catch, or use try/catch around await.

What LoopLens simulates

LoopLens uses its own promise implementation that follows the ECMAScript rules, including the extra microtask turns that .finally and returning a promise from .then cost, so the order of output matches real JavaScript. It supports new Promise, Promise.resolve, reject, all and race; Promise.allSettled and Promise.any are not supported yet. Timers use a virtual clock, and real network requests are not simulated. See how all of this fits together in the event loop guide.

Next: JavaScript closures, explained →

Open the visualizer with your own code