JavaScript interview questions: can you run the interpreter in your head?
Ask someone to define a closure and you'll get the same three sentences everyone gets from a guide they read once. Ask them what a loop full of var and setTimeout actually prints, and you find out whether they've been burned by it.
The six questions below aren't really six separate topics. JavaScript has no compiler standing between you and the runtime, so almost everything senior candidates get asked is a version of the same test: can you predict what actually happens when this code runs, not what you meant it to do.
Candidates who can hold that model in their head answer all six. Candidates who memorized definitions answer the first half and then start guessing. If you'd rather work through the topic than read about it, the JavaScript track covers the same ground one idea at a time.
Question 1: what logs first, the timeout or the promise?
This is one of the oldest tricks in a JavaScript round and it still works, because most people learn 'async code runs after sync code' as a slogan rather than as a mechanism with parts you can name.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// logs: 1, 4, 3, 2Why does the promise log before the zero-millisecond timeout?
Junior answer
Promises are just faster than setTimeout, so they get to run first even when both are scheduled at roughly the same time.
Senior answer
There's one call stack, and it runs to completion before anything queued gets a turn. Once it's empty, the microtask queue (promise callbacks, including everything after an await) drains completely before the event loop even looks at the next macrotask (timers, I/O, UI events). Nothing about speed is involved: setTimeout(fn, 0) doesn't mean 'run now', it means 'join the macrotask queue', and that queue is checked only after every microtask has run.
The follow-up is usually what happens if a promise chain keeps scheduling more microtasks off the back of each other: the macrotask queue never gets a turn, the browser can't paint, and the tab looks hung with nothing in the code technically wrong.
Worth saying without being asked: await doesn't pause the language, it desugars to a .then. Every await point is a microtask boundary, which is why a loop full of sequential await calls runs one after another even though nothing looks blocking, and why wrapping independent calls in Promise.all instead of awaiting each in turn is the fix people reach for once they've actually seen the ordering.
Question 2: fix the loop that logs the wrong number three times
This one shows up as a live task almost as often as it shows up as a question, because writing the fix proves you understand the bug better than describing it does.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs 3, 3, 3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs 0, 1, 2Why does switching var to let fix it, mechanically?
Junior answer
let is block-scoped, so it works properly inside a loop and var doesn't.
Senior answer
var has one function-scoped binding for the whole loop, so all three timeouts close over the same variable, and by the time any of them run the loop has already finished with i at 3. let creates a fresh binding for every iteration, so each closure captures its own i, frozen at the value it had that pass. Block scoping is true but it's not the mechanism, the per-iteration binding is.
If they mention memory here, that's a natural moment to ask when closures cause leaks. A closure keeps its captured scope reachable for as long as the closure itself is, which is the same fact behind this bug and behind a real leak in a long-lived app.
Question 3: why is this undefined inside the callback?
A method that works fine when you call it directly and breaks the moment it's passed as a reference. This is the bug that teaches people what this actually means, usually the hard way, the first time it happens in real code.
class Counter {
count = 0;
increment() { this.count++; }
}
const counter = new Counter();
button.addEventListener("click", counter.increment);
// this is undefined when the handler firesthis is set by how a function is called, not by where it's written. counter.increment() and counter.increment passed to addEventListener are the same function, but only the first one calls it as counter's method. The second hands the browser a bare function, and when the click fires, nothing about that call mentions counter, so this is undefined inside a class method (or the global object outside strict mode). The fix is binding in the constructor, wrapping the call in an arrow function, or writing increment as an arrow class field in the first place.
Why does an arrow method behave differently from a normal method under inheritance?
Junior answer
Arrow functions don't have their own this, so they just use whatever this is in the surrounding scope, which fixes the binding problem.
Senior answer
That's true, and it's also why arrow methods don't participate in the prototype chain the way normal methods do. An arrow class field is created fresh per instance, in the constructor, with this captured at that moment, so a subclass can't override it through the prototype the way it overrides a regular method. You've traded a real inheritance bug for a real memory cost: every instance now carries its own copy of the function instead of sharing one off the prototype.
This is the question where naming the trade-off matters more than picking a side. Arrow fields are the right default for event handlers precisely because they can't be overridden out from under you; that's also exactly why they're the wrong choice for anything meant to be extended.
Question 4: what does the caller see after this function runs?
A function mutates something it was handed. The question is whether the person calling it knows that, and whether the candidate does.
function addItem(cart, item) {
cart.items.push(item);
return cart;
}
const original = { items: [] };
const result = addItem(original, "book");
result === original; // true, and original.items now has one item tooPrimitives are copied on assignment or on a function call. Objects and arrays are not, what gets copied is the reference, so the function and the caller are looking at the same underlying structure the whole time. Anyone who's surprised by that has usually only ever seen it go wrong once it's three functions deep, which is exactly why interviewers ask it directly instead of waiting for it to come up.
Does spreading an object make a deep copy?
Junior answer
Yes, spreading creates a new object, so changes to the copy don't touch the original.
Senior answer
Spread copies one level. Top-level keys get new slots, but if any of those values are themselves objects or arrays, the copy and the original point at the exact same nested structure. const copy = { ...state }; copy.user.name = 'x' changes state.user.name too, because user was never copied, only referenced again. JSON.parse(JSON.stringify(x)) goes deeper but drops functions and Dates and undefined and breaks on anything circular. structuredClone is the honest built-in answer for a real deep copy, and it still can't clone a function.
Question 5: where does a class method actually live?
class reads as a different language feature from everything above it, and that's the point of the question: it isn't one. It compiles down to the same prototype chain that's been there since the beginning.
Every object has an internal link to another object it falls back to when a property lookup misses, and that chain keeps going until it hits null. A method defined in a class body lives once, on the prototype, shared by every instance, which is different from an arrow class field: those get copied per instance, as question 3 covered. Looking a method up on an instance walks the chain until it finds increment on Counter.prototype, and every instance sees the same function.
- extends wires one prototype's fallback to another, so a subclass instance's lookup chain runs through the child's methods, then the parent's, then Object.prototype.
- super.method() calls the parent's version explicitly, which you need the moment you override something but still want the original behavior inside the new one.
- Modifying a built-in prototype (adding a method to Array.prototype, say) changes every array in the program, including ones from libraries that never expected it, and collides with anything the spec adds under the same name later. It's a real technique and also a real way to make a codebase unpredictable.
Two that show up before the real questions start
- == vs ===. === skips coercion entirely. == runs a set of rules first, and a couple of the results surprise people who only use it by habit: null == undefined is true, but null == 0 is false, and NaN never equals anything, including itself, which is why you check for it with Number.isNaN rather than a comparison.
- Hoisting and the temporal dead zone. var, let, const and function declarations are all hoisted, but they don't behave the same once you're inside that hoisted space. var initializes to undefined immediately, so reading it early just gives you undefined. let and const are hoisted but sit in the temporal dead zone until their declaration line actually executes, and reading them before that throws a ReferenceError. That's deliberate: it catches use-before-initialization as an error instead of letting it silently return undefined.
The live task: write a debounce
Most rounds have one small keyboard moment, and for JavaScript it's usually this one, or throttle's sibling version of it. What's being scored isn't whether the syntax lands. It's the order you build it in.
- Say the problem before touching the keyboard: a rapid-fire event is triggering expensive work on every call, and you want it to run once, after things go quiet.
- Reach for a closure holding a single timer id. It's the same mechanism as question 2, and saying that out loud is a good sign, not a distraction.
- Clear the pending timer on every call, then schedule a new one. That line is the whole mechanism. Throttle is the same shape with a boolean guard and a fixed interval instead of a reset.
- Decide what happens to this and the arguments the wrapped function was called with. Forwarding them with apply, or a rest parameter and an arrow function, is what makes the debounced version actually interchangeable with the original.
- Say which one fits which case. A search box wants debounce, since it's waiting for a pause. A scroll handler wants throttle, since it wants steady progress rather than silence followed by a burst.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}The implementation is a few lines. The narration is the part that's hard to rehearse alone, because the follow-up is the interview, and the follow-up here is almost always how you'd add a leading-edge option or cancel a pending call.
Question 6: what's keeping this object alive?
A closing question that sounds abstract and isn't. Every long-lived JavaScript app leaks eventually, and the question is whether a candidate can point at where.
Garbage collection is reachability-based: an object is kept as long as something reachable from a root still points to it, and reclaimed once nothing does. Setting a variable to null doesn't free anything by itself, it just removes one path to the object, and if another reference exists elsewhere, nothing changes. The usual leaks in a real app are forgotten event listeners, timers that outlive what they were scheduled for, closures retaining a large object they only needed briefly, and DOM nodes removed from the page but still referenced from JavaScript, which keeps the whole detached subtree alive.
How does a WeakMap avoid a leak a Map would cause?
Junior answer
A WeakMap is basically a faster Map for when your keys are objects.
Senior answer
A WeakMap holds its keys weakly, so an entry doesn't count as a reason to keep the key alive. If you're caching computed data per DOM node in a regular Map, that map keeps every node reachable for as long as the map exists, even after the node's been removed from the page, which is a slow leak with a completely ordinary-looking cause. The same cache in a WeakMap lets the node and its cached value get collected together the moment nothing else references the node. WeakRef solves a narrower version of the same problem, holding a reference to a value without being a reason it survives, and it comes up far less often.
Common questions
How much of this overlaps with a React round?
More than it looks like on paper. Stale closures inside useEffect, an event handler losing this, async ordering inside a component: all of it is React sitting directly on top of the mechanisms above, not a separate topic.
Do generators and Proxy come up?
Occasionally, and almost never as 'implement one'. The realistic version is naming a use case: a generator for a lazy or infinite sequence, or paging through an API without loading it all at once; a Proxy for validation or a reactivity system that needs to notice when a property is read or written. Knowing what each one is for is worth more than the syntax.
I mostly write TypeScript at work. Does plain JavaScript still get asked?
Yes, because types are erased at compile time and none of this is a type-system question. A TypeScript round checks a different layer entirely, and it assumes the runtime layer underneath it is already solid.
Is it fine to say I'd reach for lodash's debounce instead of writing one?
In real code, yes, and saying so is a normal engineering answer. In the interview, they're not asking whether you'd use the library, they're using the exercise to see the closure-and-timer model in your hands, so build it anyway and mention the library as what you'd actually ship.