Output-based JS questions on Promises and async behavior
Classic 'what's the output' problems hinge on: microtask queue order (Promises before setTimeout), synchronous code first, async function continuations are microtasks, await suspends even when value is already resolved, Promise.resolve().then(...) doesn't immediately invoke. Trace stack → micro → macro and you'll predict any ordering.
Mental model
- Synchronous code runs first until the call stack is empty.
- Microtasks (Promise
.then/.catch/.finally, async function continuations after await,queueMicrotask) drain completely. - One macrotask (setTimeout/setInterval/I/O) runs.
- (Browser) Render frame if needed.
- Microtasks drain again.
- Repeat.
Classic examples
1. Order
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");Output: A, D, C, B
- Sync first: A, D.
- Microtask: C.
- Macrotask: B.
2. await suspends even on resolved
async function f() {
console.log("1");
await null;
console.log("2");
}
console.log("start");
f();
console.log("end");Output: start, 1, end, 2
- f() runs synchronously until the await.
- The continuation after await schedules as a microtask.
- Sync code finishes (end), then microtask runs (2).
3. Chained .then microtasks
Promise.resolve()
.then(() => console.log("A"))
.then(() => console.log("B"));
Promise.resolve()
.then(() => console.log("C"))
.then(() => console.log("D"));Output: A, C, B, D
- First
.thenof each Promise queues first round: A, C. - Each schedules the next: B, D queued.
- All microtasks drain in queue order.
4. async + setTimeout interleave
async function f() {
console.log("1");
await Promise.resolve();
console.log("2");
}
setTimeout(() => console.log("3"), 0);
f();
console.log("4");Output: 1, 4, 2, 3
- Synchronous part of f(): 1.
- 4.
- Microtask: 2.
- Macrotask: 3.
5. for-await-of
async function f() {
for (const n of [1, 2, 3]) {
await new Promise(r => setTimeout(r, 100));
console.log(n);
}
}Sequential — each await waits 100ms. Total ~300ms. Use Promise.all(arr.map(...)) for parallel.
6. forEach + async — does NOT wait
[1, 2, 3].forEach(async (n) => {
await delay(100);
console.log(n);
});
console.log("after");Output: after, then 1/2/3 (in some order) forEach doesn't await its async callbacks. Use for..of or Promise.all(map).
7. process.nextTick vs Promise (Node)
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("tick"));
console.log("sync");Node output: sync, tick, promise, immediate
- nextTick fires before Promise microtasks within the same phase in Node.
- Promise microtasks before macrotasks (setImmediate).
8. Promise resolution chaining
Promise.resolve(1)
.then((v) => Promise.resolve(v + 1))
.then(console.log);Output: 2. Returning a Promise inside .then chains — the next .then gets the resolved value.
9. Throw vs reject
async function f() { throw new Error("x"); }
f().then(() => "ok", (e) => console.log(e.message));Output: "x". throw in async = rejection.
Heuristics
- Sync → microtask → macrotask → render → repeat.
- await always suspends, even with already-resolved value.
- forEach doesn't await.
- Promise.resolve() + .then = microtask, not immediate.
- nextTick before Promise microtasks in Node only.
Interview framing
"Trace through stack → microtask → macrotask. Sync code runs first; then microtasks (Promise continuations, async function resumes after await, queueMicrotask) drain fully; then one macrotask runs; then microtasks again. The two non-obvious facts: await always yields a microtask hop even when the value is already resolved, and forEach(async) doesn't wait for its callbacks. In Node, process.nextTick outranks Promise microtasks but both still beat setTimeout."
Follow-up questions
- •Why does await yield even for already-resolved values?
- •Why doesn't forEach wait for async callbacks?
- •Compare process.nextTick and Promise microtasks.
Common mistakes
- •Assuming setTimeout(0) runs before Promise.then.
- •Using forEach with async.
- •Sequential awaits for independent work.
Performance considerations
- •Microtask floods can block paint. Move heavy work to setTimeout or Workers.
Edge cases
- •Microtask starvation (infinite chain blocks render).
- •Node phases for setImmediate vs setTimeout(0).
- •Returning a Promise from .then auto-chains.
Real-world examples
- •Classic interview trick questions, debugging weird timing bugs.