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.
Code (current line highlighted)
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.
Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.
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.
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.
Console output
Scope and variables (yellow = changed this step)
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");Promise.resolve(1)creates a promise that is already fulfilled with1.- The first
.thencallback joins the microtask queue right away, because its promise is settled. It does not run yet. Each.thenalso returns a new promise, which the second.thenwaits for. console.log("sync")runs and the script finishes.- 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. - 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
.thenreturns 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 receivesundefinedand does not wait for any work started in the callback. - Nesting instead of chaining. Return the inner promise and add the next
.thento 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 usetry/catcharoundawait.
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.