JavaScript Interview Masterclass

Lesson 1 of 8

The Output Questions (What Does This Log?)

"It is a capital mistake to theorize before one has data." — Arthur Conan Doyle, A Scandal in Bohemia

Somewhere in almost every JavaScript interview, the whiteboard clears and a small, innocent-looking snippet appears with one question underneath: what does this log? No API to memorise, no algorithm to invent — just five or six lines and a promise that they hide a trap. This is the interviewer's favourite move because it cannot be faked. You either understand how the language actually executes — hoisting, closures, this, coercion, the event loop — or you guess. This chapter opens Part VIII by turning the whole Volume into interview readiness: we collect the classic "what does this log?" puzzles and, for each, give the answer and the precise mechanical reason — cross-referenced back to the chapter that taught it.

The question this chapter answers

When an interviewer shows you a tricky snippet and asks "what does this log?", can you produce the exact output and narrate the mechanism that produces it — every time, not by luck?

Core Concepts

Output questions are diagnostic tools. Each one is engineered to fail if you hold a common misconception. Get the answer right for the wrong reason and a good interviewer will find you out with a follow-up. So the goal is never to memorise outputs — it is to see the machine. We will walk through six families of trap, and for each the rule that makes the output inevitable.

1. var in a loop with setTimeout

The most famous trap in the language. Predict the output, then run it:

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Why (Part II — closures & scope). var is function-scoped: there is exactly one i shared by all three callbacks. The loop finishes — driving i to 3 — long before any setTimeout callback runs (they are deferred to a later tick). So all three closures read the same, now-final i. let is block-scoped: the loop creates a fresh binding of j per iteration, and each callback closes over its own copy — frozen at 0, 1, 2. The classic pre-let fix was an IIFE per iteration to manually capture the value; let bakes that fix into the language.

2. Hoisting and the Temporal Dead Zone

console.log(foo());   // "early!"  — function declarations are fully hoisted
function foo() { return "early!"; }

console.log(v);       // undefined — the *declaration* hoists, not the value
var v = 5;

console.log(x);       // ReferenceError: Cannot access 'x' before initialization
let x = 1;

Why (Part II — hoisting & the TDZ). All declarations are processed before any line runs. A function declaration is hoisted whole, so it is callable at the very top. A var is hoisted but initialised to undefined until its assignment line executes — hence reading it early yields undefined, not an error. A let/const is also hoisted, but into the Temporal Dead Zone: it exists yet cannot be touched until its declaration runs, so an early read throws ReferenceError. The TDZ is a feature — it turns a silent undefined bug into a loud crash.

3. this surprises

this is decided by how a function is called, not where it is defined — except for arrow functions, which have no this of their own. Run this:

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Why (Part II — the this keyword). obj.regular() uses the call-site rule: the thing left of the dot becomes this, so this === obj. Assign the same function to a bare variable and call it, and there is no object left of a dot — this is undefined (strict) or the global object (sloppy), so this === obj is false. The arrow inside Box never gets its own this; it permanently captures the this that was in scope when it was created — the new instance — so detaching and re-calling it changes nothing. This is exactly why arrows are the right tool for callbacks that must remember their surrounding this.

Everyday analogy

this is like the word "here." Shout "clean up in here!" and which room you mean depends on where you are standing when you shout — that is a regular function, bound at the call site. An arrow function is a note you wrote down earlier that says "clean up in the kitchen": it names the room at writing time and means the same room no matter where you later read it aloud. Detaching a method is walking into a different room and shouting "here" — you clean the wrong place, or nowhere at all.

4. Coercion traps

JavaScript's + operator is overloaded (concatenate or add) and == performs type coercion. That produces some legendary outputs:

Expression Result Why
[] + [] "" Both arrays coerce to string; "" + "" === ""
[] + {} "[object Object]" []→"", {}→"[object Object]", concatenated
1 + "2" - 1 11 1 + "2" → "12" (string), then "12" - 1 → 11 (number)
null == undefined true The one special == rule: they equal each other and nothing else
0.1 + 0.2 0.30000000000000004 IEEE-754 floating point can't represent 0.1 exactly
JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Why (Part I — types & coercion). When either operand of + is a string, + concatenates; otherwise it adds. Objects and arrays reduce to primitives first via toString: [] becomes "", {} becomes "[object Object]". The - operator has no string mode, so it always coerces to number — which is why 1 + "2" - 1 swings from string back to number and lands on 11, not 2. And 0.1 + 0.2 is not a JavaScript quirk but an IEEE-754 one shared by nearly every language: those decimals are infinite in binary, so they round. Never compare floats with ===; compare within a small epsilon.

5. Event-loop ordering: sync, microtask, macrotask

The single most common async output question. Predict the order, then run it:

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Why (Part III — the event loop). Synchronous code runs first, to completion — so A and D log immediately, in order. Then the loop drains the microtask queue (resolved promises) entirely before touching the macrotask queue (timers). So the promise callback C runs before the setTimeout callback B, even though setTimeout(…, 0) looks "immediate." The mantra: sync → all microtasks → one macrotask → all microtasks → …. setTimeout(0) does not mean "now"; it means "after the current work and every pending microtask."

Now the harder cousin — async/await ordering. await splits a function: everything after it becomes a microtask.

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Why (Part III — async/await). An async function runs synchronously up to its first await — so 1 logs during the f() call. await then suspends f and queues its continuation as a microtask, returning control so 2 logs. The .then on line below queues its callback as a microtask too — but f's continuation was queued first, so 3 runs before 4. await is not magic; it is "wrap the rest of this function in .then."

6. Reference vs value

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Why (Part I — primitives vs references). Objects are held by reference: b = a copies the reference, not the object, so both names address one object and a mutation through either is visible through the other — including through a function parameter, since arguments are the same references. Primitives (number, string, boolean, …) are held by value: reassigning the parameter n rebinds a local copy and cannot reach the caller's x. Note the subtlety: reassigning a parameter never affects the caller, but mutating an object a parameter points to does.

History changed here

The var/let loop trap is so common it shaped the language. Before ES2015, capturing the loop variable required a hand-rolled IIFE — (function(i){ setTimeout(…) })(i) — a pattern every JS developer once memorised. When TC39 designed let, they deliberately specified a fresh binding per loop iteration precisely so this whole class of bug would disappear. The trap survives in interviews as a fossil: answer it well and you prove you understand both the modern fix and the closure mechanics underneath it.

The engineer's notebook

How to actually attack an output question, live:

• Classify each line first. Sync? Timer (macrotask)? Promise/await (microtask)? Sort the timeline before you read values.
• For this, look left of the dot. No dot, no object — this falls back to undefined/global. Arrows ignore the call site entirely.
• For +, ask "is either side a string?" If yes it concatenates; -, *, / always go numeric.
• For loops, ask "one binding or many?" var = one shared; let = one per iteration.

Real-world example — chika.ng Academy

A real bug from the wild: a dashboard fired off for (var i = 0; i < lessons.length; i++) loops that attached onclick handlers logging lessons[i].id — and every button reported the last lesson's id, because all handlers closed over the one shared i. The fix was a one-word change: var → let. The same event-loop rules explain why a "save progress" toast scheduled with setTimeout(0) always appears after the promise that writes the quiz score resolves — microtasks first, timers second. These are not trivia; they are Tuesday.

Common Mistakes

  • Assuming setTimeout(fn, 0) runs "immediately." It runs after all synchronous code and every queued microtask. Promises always beat timers.
  • Thinking let "fixes closures." Closures were never broken — let just gives each iteration its own binding to close over. Know the real mechanism, not the slogan.
  • Reading this from where a function is written. For regular functions this is set at the call site. Only arrow functions use lexical this.
  • Trusting ==. null == undefined is true, but 0 == "", [] == ![], and friends are minefields. Use === and coerce deliberately.
  • Comparing floats with ===. 0.1 + 0.2 !== 0.3. Compare with an epsilon or work in integers (e.g. cents, not naira).
  • Confusing reassignment with mutation. Reassigning a parameter never touches the caller; mutating the object it points to does.
Thought experiment

Take the async/await snippet above and add await null; a second time inside f, with a log between them. Where does that new log land in the order? Each await yields to the microtask queue and lets already-queued microtasks run first — so every extra await pushes the code after it one more lap around the microtask loop. Trace it on paper before you run it: the discipline of predicting, then checking, is exactly the skill the interview is measuring.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What does a for (var i…) loop with three setTimeout callbacks log, and why?"
Model answer — "It logs the final value three times — 3, 3, 3. var is function-scoped, so all three callbacks share one i; the loop finishes and sets i to 3 before any callback runs, so they all read 3. Switching to let gives each iteration its own binding and you get 0, 1, 2."
Follow-up they'll throw: "Fix it without let." → Wrap the body in an IIFE that takes i as an argument, capturing a copy per iteration.
Intermediate
"Order these: a console.log, a setTimeout(…,0), and a Promise.resolve().then."
Model answer — "Synchronous log first. Then the event loop drains all microtasks before any macrotask, so the promise .then runs next, and the setTimeout last. setTimeout(0) is a macrotask — it waits behind the entire microtask queue, not just the sync code."
Follow-up: "Now put an await in the mix — where does the code after it run?" → As a microtask, in the same queue as .then, ordered by when it was queued.
Senior
"A teammate's output-question answer is right but their explanation is wrong. How do you tell, and why does it matter in production?"
Model answer — "I probe with a variation — add a second await, or detach a method — and see if their model still predicts correctly. A right answer from a wrong model breaks the moment reality shifts: someone who thinks let 'copies the value' will write a subtle bug the day they close over a mutable object instead. These snippets are proxies for the real skill — an accurate mental model of hoisting, closures, this, and the event loop — because that model is what prevents the 3am incident, not the trivia answer itself."
The senior move: treat output questions as a lens on someone's mental model, not a memory test — and hold your own answers to the same standard.
Company lens — how the same topic gets asked
micro1Live and conversational — the AI interviewer wants you narrating the timeline out loud. Classify each line (sync / micro / macro) as you read it; a confident wrong-then-corrected trace beats silence.
GoogleExpects the exact output and the spec-level reason — "fresh binding per iteration," "microtask queue drains fully." Vague hand-waving invites a harder variation.
MicrosoftValues a clean, structured trace: state, timeline, output — in that order. They watch your method more than your memory.
Amazon / AWSTies it to real bugs: "where would this closure/coercion trap actually bite in a service?" Connect the snippet to a production failure mode.
Hands-on exercise
  1. In the editor above, run the var/let loop, then rewrite the var version to also print 0, 1, 2 without using let — use an IIFE. Confirm the output.
  2. Run the event-loop block, then add a second Promise.resolve().then and a second setTimeout. Predict the full order before running, then verify.
  3. Out loud (as in a micro1 screen), trace the async/await snippet line by line, saying "sync", "microtask", or "macrotask" for each log as you go.
  4. Write one coercion expression whose output surprises a colleague, then explain it in a single sentence using the "is either side a string?" rule.

Chapter Summary

  • Output questions test your mental model, not your memory — the goal is to see the machine, then narrate it.
  • var in a loop shares one binding (all callbacks read the final value); let gives a fresh binding per iteration.
  • var hoists to undefined; let/const hoist into the TDZ (early read throws); function declarations are callable early.
  • this is set at the call site for regular functions; arrows capture lexical this and ignore the call site.
  • + concatenates if either side is a string; -/*// always go numeric; 0.1 + 0.2 !== 0.3 (IEEE-754).
  • Execution order is: all sync → all microtasks (promises, await continuations) → one macrotask (timers) → repeat.
  • Objects pass by reference (mutations leak); primitives pass by value (reassignment doesn't).
Narrative bridge

Output questions prove you understand how JavaScript runs. The next tier of interview asks whether you understand it deeply enough to rebuild it — and no trio tests that like bind, call, and apply, the very machinery behind the this traps you just decoded. Next chapter: Build Your Own bind, call & apply — where we implement them from scratch and turn "I know what this does" into "I can make this do anything."

Editorial reflection — the "what does this log?" question feels like a gotcha, and junior engineers resent it. But it is the most honest question in the interview: it cannot be bluffed, it rewards understanding over recall, and every trap in it is a real bug someone once shipped. You have now walked the whole Volume — types, scope, closures, this, the event loop — and these puzzles are simply those chapters wearing disguises. Learn to see through the disguise, narrate the mechanism out loud, and the finale of your interview becomes the easiest part.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.