JavaScript Output-Based Questions
How to dry-run this, closures, hoisting, and the event loop on a whiteboard. A method, not a dump of 50 puzzles you will forget.
- interview
- javascript
- output-questions
Output questions fail when you stare at the snippet and guess “promise then timeout.” The interviewer wants a trace. If you cannot point at the call stack, the microtask queue, and the timer queue, you will miss the third console.log and then argue with the runtime.
This is a method for dry-running this, closures, hoisting / TDZ, and the event loop on a whiteboard. Three original traces sit below so you can practice the method. It is not a dump of fifty puzzles.
Key takeaways
- Annotate: stack, then microtasks, then timers. Never skip the current task.
thisis call-site for ordinary functions, lexical for arrows. Promise jobs are not method calls.typeofdoes not save you from the TDZ.varis undefined,letthrows.- Write the output list before you defend it. Then check one footgun.
Narrate the stack, then the queues
Keep four columns. Update them after every statement that schedules work.
| Column | Holds |
|---|---|
| Stack | What is running now (LIFO) |
| Microtasks | Promise reactions, queueMicrotask, await continuations |
| Timers / tasks | setTimeout, many DOM events |
| Output | What console.log already printed |
Rules you actually need (browser-simplified; the HTML spec is more detailed):
- Run the current task until the stack is empty.
- Drain all microtasks, including ones queued while draining.
- Optionally render.
- Take the next timer/task.
Sync console.log prints during step 1. Promise.then prints in step 2. setTimeout(0) prints in step 4. Details: the event loop, queueMicrotask vs setTimeout.
Procedure on the whiteboard:
- Circle every declaration. Mark
var(initializedundefined),let/const(TDZ until the line), functions (hoisted as callable). - Number the logs. Do not number scheduled callbacks until they run.
- When you see
fn(), decidethisat that call site. - When you see
await, freeze the async function and enqueue its continuation as a microtask. - Only then drain queues in order.
Example 1: await vs a sibling microtask
async function paint() {
console.log('1 enter');
await null;
console.log('4 after await');
setTimeout(() => console.log('6 timeout'), 0);
await Promise.resolve();
console.log('5 after second await');
}
console.log('0 start');
paint();
Promise.resolve().then(() => console.log('3 sibling microtask'));
console.log('2 sync end');
Trace:
0 start(stack)paint()runs,1 enter.await nullwraps withPromise.resolveand enqueues the continuation.paintreturns a pending promise.- Sibling
.thenenqueues after that continuation. 2 sync end- Microtask 1:
4 after await, schedule timeout,await Promise.resolve()enqueues the second continuation - Microtask 2:
3 sibling microtask - Microtask 3:
5 after second await - Timer:
6 timeout
Output:
0 start
1 enter
2 sync end
4 after await
3 sibling microtask
5 after second await
6 timeout
The footgun: people print 3 before 4 because “promises are FIFO and I wrote the sibling second.” The await continuation was queued first. await on a non-thenable is still a microtask tick. See promises.
Example 2: this inside then
const api = {
label: 'checkout',
ping() {
console.log('sync', this.label);
Promise.resolve(this.label).then(function (v) {
console.log('then fn', this && this.label, v);
});
Promise.resolve(this.label).then((v) => {
console.log('then arrow', this.label, v);
});
},
};
api.ping();
Trace:
api.ping()is a method call →this === api.sync checkout- Both
thencallbacks are scheduled as microtasks. The valuevis already'checkout'(captured fromthis.labelon the stack). - Ordinary
functionas a reaction: not called as a method, notnew, notbind. In strict code (modules),thisisundefined.this && this.labelisundefined(does not throw). - Arrow: lexical
thisfromping, stillapi.
Output:
sync checkout
then fn undefined checkout
then arrow checkout checkout
If someone wrote this.label in the ordinary function, it throws in strict mode and you never see the arrow log. Call-site rules: this binding, call, apply, and bind.
Example 3: TDZ typeof and let in a loop
function quiz() {
console.log(typeof a);
try {
console.log(typeof b);
} catch (e) {
console.log('tdz');
}
var a = 1;
let b = 2;
const fns = [];
for (let i = 0; i < 2; i++) {
fns.push(() => i);
}
console.log(fns[0](), fns[1]());
}
quiz();
Trace:
var ais hoisted and set toundefined.typeof ais'undefined'.let bis hoisted into TDZ.typeof bis still a read ofb→ReferenceError. Catch printstdz.- After init,
a === 1,b === 2. for (let i ...)creates a newibinding per iteration. The arrows close over 0 and 1, not the final 2.
Output:
undefined
tdz
0 1
typeof is not a safe probe for let/const. That is the whole TDZ lesson. See temporal dead zone, hoisting, var vs let vs const, closures.
If the loop had used var i, both arrows would print 2. Do not mix that rewrite mid-trace. Finish the snippet you were given.
Footguns (beyond the three traces)
setTimeout(fn, 0)is not “next line.” It is the next task.- Microtasks can starve rendering if you keep queueing more microtasks in a loop.
finallyon a promise still runs as a reaction. A return value fromfinallyis discarded; the original fulfillment or rejection is what the nextthensees. Iffinallythrows (or returns a rejecting thenable), that error replaces the original.- Extracting
obj.methodand calling it later dropsthis. - Default parameters read other bindings in the TDZ if you reference a later
let.
When the snippet mixes two systems (TDZ + timers), finish declarations and sync logs first. Queues only run after the current task.
Interview out-loud answer
“I treat output questions as a trace, not a quiz. I mark bindings, then run the stack to empty, then drain microtasks, then timers. Await continuations enqueue when the await is evaluated, so they can beat a Promise.then written later in the same task. For this, I use call-site for functions and lexical for arrows. For let, I assume ReferenceError until the initializer, including under typeof.”
Practice more event-loop mixes after you can recap those three without looking. Then add strict mode and arrow functions if the snippet uses them.