InterviewStack.io LogoInterviewStack.io
Interview Prep17 min read

Full-Stack Developer JS/TS Interview: The Loop Isn't the First Crash

The loop looks like the classic var bug. It's not what crashes this mid-level Full-Stack Developer JavaScript and TypeScript interview first.

IT
InterviewStack TeamEngineering
|

One Timeout Callback, Two Independent Failures

Show almost any mid-level engineer for (var i = 0; i < 3; i++) { setTimeout(fn, 10) } and the diagnosis comes fast: var is function-scoped, so all three callbacks share one i binding, and the loop should log the same wrong value three times instead of 0, 1, 2. This walkthrough follows one real interview built from the same interview_package structure InterviewStack.io's AI mock interviewer runs against today, and that fast, correct-sounding diagnosis is also an incomplete one: run this interview's actual code and it never gets the chance to log a wrong value at all, because a second, independent failure in the same three lines crashes it first.

Milo, a mid-level Full-Stack Developer, gets the full file at minute zero: the RequestTracker class above, and a generic mergeDefaults utility below it, both pulled from a production incident review in a Node-backed full-stack app. The class hides a this that Node quietly rebinds to its own internal timer object inside the timeout callback, for a reason that has nothing to do with the loop. The function hides a TypeScript type that says one of its fields can never exist, while the value that field actually holds at runtime is undefined. Two different language guarantees break in two different ways in the same handful of lines, and the interview spends its first ten minutes finding out whether Milo catches both, or stops after the one that's easy to name.

If the rule for how this gets assigned still feels shaky before diving in, the question bank has focused practice on just that piece.

Key Findings

  • This is a 30-minute mid-level interview scored across 4 rubric dimensions worth 100 points total: 30 for Interviewer Objectives Alignment, 30 for Level-Specific Expectations, 20 for Technical Proficiency, 20 for Communication & Problem Solving.
  • The interview runs in 3 timed phases: runtime reasoning and issue discovery (0-10 min), refactor proposal and trade-off discussion (10-22 min), and type design and testing depth (22-30 min).
  • 15 checklist items split evenly across the three phases, 5 per phase.
  • The same three-line loop hides two independent failures: a shared var i binding across all three setTimeout callbacks, and a this that Node rebinds to its internal Timeout object instead of the class instance inside that same callback.
  • Because the this failure fires first, the callback's this.log(...) call itself throws before it can run, so none of the three scheduled callbacks ever prints the wrong i value.
  • TypeScript infers the merge utility's retries field, 1 of the 2 fields in the defaults object, as type never, while the actual runtime value after the merge is undefined, a compile-time promise the runtime quietly breaks.
  • 4 topics are explicitly out of scope: React, Vue, or Angular framework patterns, distributed-systems design, database or SQL work, and algorithm-heavy dynamic programming or graph problems.

The interview question

You're reviewing the following TypeScript utility and class after a production issue in a Node-backed full-stack app.

class RequestTracker {
  private count = 0;
  label = 'tracker';

constructor(private readonly log: (msg: string) => void) {}

start() { for (var i = 0; i < 3; i++) { setTimeout(function () { this.count++; this.log(${this.label}:${i}:${this.count}); }, 10); } } }

function mergeDefaults<T extends object, U extends object>(defaults: T, overrides: U): T & U { return { ...defaults, ...overrides }; }

const tracker = new RequestTracker(console.log); const config = mergeDefaults( { retries: 3, headers: { traceId: 'abc' as string | null } }, { retries: undefined } );

tracker.start();

Walk me through what concerns you, what behavior you'd expect at runtime, and how you would change this code so it is safer and easier to maintain.

Run the snippet as written and the crash comes first, and it comes from the class, not the loop. start()'s setTimeout callback is a plain function, not an arrow function, so it has no this of its own, it takes whatever receiver its caller supplies. That's a real question, because class bodies are always strict mode in JavaScript, and strict mode changes what a bare, receiver-less call resolves this to. But setTimeout isn't a bare call: in Node, the runtime this scenario is explicitly built around, the timers implementation invokes the callback with this bound to the internal Timeout object that represents the scheduled timer, the same object setTimeout itself returns, and it does this regardless of strict mode. this.count++ doesn't throw: count isn't a property of a Timeout object, so this.count reads as undefined, undefined++ silently evaluates to NaN, and a new count property holding NaN gets written onto the timer object. The TypeError fires one statement later, at this.log(...), because Timeout objects have no log method: this.log is not a function. Either way, the callback dies calling this.log(...), so the wrong i value it would have printed never reaches a log line. But the mechanism is Node's timer implementation choosing an unhelpful receiver, not strict mode producing undefined, and in a default Node setup, an uncaught synchronous throw inside a callback like this one terminates the process, so the other two scheduled timers may never even get a turn.

The interviewer isn't grading whether Milo can name the classic loop bug. The interview's objectives care about five things: the this-binding failure and its runtime consequences, the function-versus-block-scope reasoning behind the shared i binding, recognizing that mergeDefaults has misleading typing and shallow merge semantics, proposing changes that fix both runtime correctness and type clarity rather than just one, and communicating clearly about what JavaScript actually does versus what TypeScript can and can't guarantee.

The Rubric Rewards the Fix, Not Just the Diagnosis

A mid-level bar here doesn't mean reciting the spec. The level-specific bar asks Milo to independently identify the main language pitfalls without heavy prompting (though not to design a fully generic deep-merge library from scratch), offer at least one reasonable refactor for correctness and one for API or type safety with real trade-off discussion, distinguish production-safe patterns from clever-but-fragile tricks, and read small TypeScript snippets confidently, including unions, generics, and inferred return types.

The four rubric dimensions by point weight for this interview

Framing and level-fit carry 60 of the 100 points combined, the same two dimensions the class's crash and the utility's type gap both test. Naming the right JavaScript rule without turning it into a real fix, or fixing the code without explaining why the fix matters, still caps the score well short of full marks.

The Walkthrough: Four Turns Across Two Broken Guarantees

We picked 4 of this interview's 6 real follow-up prompts, chosen to trace both failures from diagnosis to fix. Milo is a mid-level candidate working through this same scenario. The follow-ups on redesigning the utility's API for multiple teams and on what tests a mentor should require aren't dramatized turn by turn here (they're covered in the FAQ instead), but they're real reps you'll still need before sitting the live version.

Turn 1: The Environment, Not Strict Mode

Interviewer: "If this code ran in strict mode under Node or a modern build setup, what do you expect this to be inside the timeout callback, and how would that affect behavior?"

COMMON MISTAKE
Milo says "it's undefined either way, so it doesn't really matter" and jumps straight to a fix without checking what Node actually does. That skips the checklist item this phase is built around, explaining that a regular function gets its own `this` based on call-site, and here it will not refer to the class instance, and it also gets the specific value wrong: in Node, `this` inside a `setTimeout` callback isn't `undefined` at all.
STRONGER MOVE
Name the actual mechanism: the callback passed to `setTimeout` is a plain `function`, so it has no `this` of its own, it takes whatever receiver its caller supplies. In Node, that caller is the timers implementation, and it invokes the callback with `this` bound to the internal `Timeout` object representing the scheduled timer, not `undefined` and not the class instance, regardless of strict mode. That's a Node-specific runtime choice, not a strict-mode consequence.

Turn 2: One Fix Isn't Both Fixes

Interviewer: "Would your fix for the loop preserve the intended logging order and values, and what alternatives would you consider besides the most obvious one?"

COMMON MISTAKE
Milo swaps `var i` for `let i`, calls the loop fixed, and moves on, without noticing the callback is still a plain function, and Node still calls it with `this` bound to its own `Timeout` object, not the tracker instance. The loop fix only guarantees each callback closes over a different `i`, if the callback ever survives long enough to reach the log line, which it still won't, so the phase's `this`-fix checklist item goes unaddressed.
STRONGER MOVE
Treat the two failures as independent and fix both: `let i` (or an equivalent block-scoped capture) fixes which value each callback closes over, while a separate change, an arrow function, `.bind(this)`, or a captured instance reference, fixes the receiver. Naming both, and confirming neither one alone is sufficient, is what this checklist item is actually asking for.

Turn 3: What the Merge Actually Promises

Interviewer: "What are the TypeScript risks in mergeDefaults returning T & U, especially when an override contains undefined or nested objects?"

COMMON MISTAKE
Milo shrugs off the return type ("`T & U` just means it has both objects' properties") and never notices that `retries` collapses to `never` in the merged type while the real value is `undefined`. That leaves this phase's core ask, explaining why `T & U` can overpromise or hide semantic ambiguity, unaddressed.
STRONGER MOVE
Walk through the inference directly: `T` contributes `retries: number` from the defaults, `U` contributes `retries: undefined` from the override, and intersecting two conflicting types for the same key collapses it to `never`, a type that claims the value can't exist. At runtime, object spread still lets the override overwrite the default, so `config.retries` is genuinely `undefined`, a real value the type system just told every caller was impossible.

Turn 4: Two Layers, Two Different Fixes

Interviewer: "How would you explain the difference between a compile-time-safe improvement and a runtime-safe improvement in this snippet?"

COMMON MISTAKE
Milo treats "fix the code" as one undifferentiated task and starts rewriting lines without separating which change fixes what TypeScript sees from which change fixes what actually happens when the code runs. That conflation is exactly what this phase checks for when it asks a candidate to separate runtime policy decisions from static typing decisions.
STRONGER MOVE
Split the two failures by the layer they live in: the loop and the `this` bug are pure runtime problems that the type checker never flags, because both are structurally valid TypeScript, while the `mergeDefaults` issue is a runtime value quietly contradicting what the type system promises. A mid-level answer names both categories explicitly, `let` and an arrow function for runtime correctness, a more honest return type or an explicit `undefined`-handling policy for type-level correctness, rather than treating "fix it" as a single, undifferentiated task.

Why Does Naming the Loop Bug Feel Like Solving It?

Every fix above reads clean on the page because there was time to read the code twice and revise the answer before anyone graded it. In the live AI mock interview, the interviewer's next follow-up lands seconds after the first answer, whether or not the reasoning is finished, and the checklist above is being tracked while the candidate is still talking. Reading this walkthrough teaches the shape of both bugs. Only running the scenario live, under that clock, tells you whether you'd actually catch the second failure, the one that isn't the loop, in time.

The Blueprint Splits Runtime Fixes From Type Fixes, Phase by Phase

Here's the full 30-minute blueprint this interview runs on, phase by phase. It's the same structure the AI mock interviewer scores against in real time, live, not after the fact.

The 30-minute interview blueprint paced into its three phases

The 30 minutes split unevenly: 10 to diagnose both failures, 12 to redesign around them, and only 8 left to prove the fixes hold under a real test.

Blueprinta strong 30-minute interview, phase by phase
1
Runtime reasoning and issue discovery 0-10
  • States that `var` is function-scoped and the callbacks share the same `i` binding
  • Predicts that the logged `i` value will not be 0,1,2 as intended
  • Explains that regular functions get their own `this` based on call-site, and here it will not refer to the class instance
  • Notes likely runtime failure or incorrect state access when reading `this.count` or `this.log`
  • Mentions that async scheduling means the loop completes before callbacks execute
2
Refactor proposal and trade-off discussion 10-22
  • Proposes a concrete fix for `this` such as an arrow callback, binding, or capturing instance reference, and can compare them
  • Proposes a concrete fix for loop capture such as `let`, extracting a helper, or iterating with a block-scoped value
  • Explains how the revised code changes runtime behavior in an observable way
  • Identifies that object spread is shallow and nested `headers` behavior is not a deep merge
  • Questions whether `retries: undefined` should erase a default, be ignored, or be rejected by the type/API
3
Type design and testing depth 22-30
  • Explains why `T & U` can overpromise the merged result or hide semantic ambiguity
  • Suggests a more honest API or type approach, such as partial overrides, explicit undefined handling, or documenting shallow merge semantics
  • Separates runtime policy decisions from static typing decisions
  • Proposes a focused test set covering callback behavior, counter increments, logging output shape, and merge edge cases
  • Keeps the solution scoped appropriately for a shared app utility rather than overengineering

Notice how little of this blueprint is "name the correct JavaScript rule." Most of the 15 checklist items are what Milo does with that rule: a redesign that survives a second call site, a type that tells the truth about what it returns, and a test that would have caught either bug before it shipped.

Run This Scenario Before the Interviewer Does

The fastest way to find out whether you'd catch the second failure under real time pressure is to run this exact scenario as a live AI mock interview, timed to the same 30-minute blueprint and scored against the same rubric above. If you want to drill the underlying language fundamentals first, the question bank has focused practice on this exact topic, and the preparation guides cover what to expect at leading tech companies more broadly. If you'd rather see how a different Full-Stack Developer scenario plays out first, the end-to-end feature design walkthrough runs the same format on API and data-model design instead of language mechanics.

FAQ

Q. What does the Full-Stack Developer JavaScript and TypeScript Fundamentals interview actually test?

It's a 30-minute mid-level interview built around one TypeScript file with two independent language failures: a for loop where three setTimeout callbacks share one var i binding, and a generic merge function whose return type can quietly become never for a field that's actually undefined at runtime. The rubric weighs Interviewer Objectives Alignment and Level-Specific Expectations at 30 points each, and Technical Proficiency and Communication & Problem Solving at 20 points each, for 100 points total.

Q. If this merge utility were used by multiple teams, how should its types or API change?

A blanket T & U return type is too optimistic once you have more than one caller. A more honest design would use overloads or conditional types that treat an explicit retries: undefined differently from an omitted key, document (and test) whether nested fields like headers deep-merge or get replaced wholesale, and make that undefined-handling behavior an explicit policy decision instead of letting object-spread order decide it silently.

Q. What tests should this code have before a junior engineer's fix ships?

Three, at minimum: one asserting that each of the three scheduled callbacks logs its own loop iteration's index (0, 1, 2) once the loop-capture fix is in place, one asserting that the callback's this resolves to the tracker instance so count actually increments across all three calls, and one asserting the real runtime value of retries when an override passes it as undefined explicitly, since the type system alone won't flag a future change to that behavior.

Q. Does fixing the shared var i binding also fix the crash?

No. Changing var i to let i fixes which value each callback closes over, 0, 1, and 2 instead of one shared value, but it does nothing about the receiver: the callback is still a plain function, and Node still calls it with this bound to its internal Timeout object instead of the tracker instance, so it still throws the moment it fires. The loop fix and the this fix are independent, and a correct answer applies both, not just the one that's easier to spot.

Q. How many checklist items does the AI interviewer track in this scenario?

15 across three phases: 5 for runtime reasoning and issue discovery in the first 10 minutes, 5 for the refactor proposal and trade-off discussion in the next 12 minutes, and 5 for type design and testing depth in the final 8 minutes.

Q. What's out of scope for this interview?

Four topics: framework-specific React, Vue, or Angular patterns, distributed-systems design beyond this utility's own runtime behavior, database schema design or SQL optimization, and algorithm-heavy dynamic programming or graph problems. The interview stays focused on core JavaScript and TypeScript language mechanics, not the surrounding stack.

Q. How can I practice this exact interview scenario live?

Run the same RequestTracker and mergeDefaults scenario as a live AI mock interview, timed to the same 30-minute blueprint and scored against the same rubric, or drill JavaScript and TypeScript fundamentals questions in the question bank first if you want to build up before taking the full scenario live.

The Fix Has to Match the Failure

This file only has one real lesson underneath both bugs: know which layer a failure lives in before you fix it. The loop and the this binding are runtime problems no type checker will catch; the merge utility's retries field is a type-level promise the runtime quietly breaks. Everything the rubric rewards, the diagnosis, the redesign, the tests, is really just Milo demonstrating that same judgment twice, in two different registers, with someone watching the clock.

Topics

full-stack developerjavascript typescript fundamentalstypescript genericsjavascript closures and thismock interview practicemid-level interview prep

Ready to practice?

Put what you've learned into practice with AI mock interviews and structured preparation guides.