← All practice sets
FoundationsPractice · 12 questions

JavaScript interview questions

The JavaScript questions that keep coming up in interviews: async ordering, scope and this, equality, copying, memory and modules. Answer out loud first, then compare.

Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.

  1. Q1Mid

    What does this print, and why? `console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);`

    Reveal model answer

    It prints 1, 4, 3, 2. The synchronous code runs to completion first, so 1 and 4 log immediately. The promise reaction is a microtask and the timer callback is a task; after the current script finishes, the microtask queue is drained completely before the event loop picks the next task, so 3 logs before 2 even though the timeout is 0.

    WHAT A STRONG ANSWER COVERS

    • Synchronous code runs to completion before any queued callback.
    • Promise reactions (then, await continuations, queueMicrotask) are microtasks.
    • setTimeout, I/O and UI events schedule tasks (macrotasks).
    • The whole microtask queue drains before the next task, so a microtask that keeps queueing microtasks can starve rendering and timers.
    • setTimeout(fn, 0) means 'no earlier than', not 'immediately'.

    RED FLAGS

    • Saying setTimeout 0 runs immediately or before the promise.
    • Believing JavaScript runs callbacks in parallel threads.
    • Not being able to name microtasks vs tasks.

    LIKELY FOLLOW-UPS

    • Where does code after an `await` run relative to a setTimeout scheduled earlier?
    • How could an infinite chain of microtasks freeze the page?

    SOURCES

  2. Q2Junior

    What is a closure? What does `for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));` print, and how do you fix it?

    Reveal model answer

    A closure is a function bundled with references to the variables of the scope where it was created, so it can still read and write them after that scope has returned. The loop prints 3, 3, 3: `var` is function-scoped, so all three callbacks close over the same `i`, which is 3 by the time the timers fire. Using `let` creates a fresh binding per iteration, so it prints 0, 1, 2.

    WHAT A STRONG ANSWER COVERS

    • Closures capture variables (bindings), not a snapshot of their values.
    • `var` is function-scoped; `let` and `const` are block-scoped, and `let` in a for loop gets a new binding per iteration.
    • Practical uses: private state, factories, memoization, event handlers, partial application.
    • Closures keep captured objects alive, which matters for memory.

    RED FLAGS

    • Saying the closure copies the value at creation time.
    • Defining a closure only as 'a function inside a function' with no mention of captured scope.

    LIKELY FOLLOW-UPS

    • How would you fix it without `let` (IIFE or a factory function)?
    • How can a closure cause a memory leak?
    • What is a stale closure in a React hook?

    SOURCES

  3. Q3Mid

    Given `const user = { name: "Ada", greet() { return this.name; } }; const greet = user.greet; greet();` what happens, and how is `this` decided in general?

    Reveal model answer

    `this` is decided by how a function is called, not where it is defined. `user.greet()` sets `this` to `user`, but the detached call `greet()` has no receiver, so `this` is `undefined` in strict mode (modules and classes are strict) and the call throws a TypeError; in sloppy scripts it falls back to `globalThis`. Fix it with `user.greet.bind(user)`, by calling through the object, or by using an arrow function where you want the enclosing `this`, since arrows have no `this` of their own and ignore call, apply and bind.

    WHAT A STRONG ANSWER COVERS

    • Method call `obj.fn()` binds `this` to `obj`; a plain call gets `undefined` (strict) or `globalThis` (sloppy).
    • `call`, `apply` and `bind` set `this` explicitly; `new` binds it to the new object.
    • Arrow functions capture `this` lexically from the enclosing scope.
    • Passing a method as a callback (event handler, setTimeout, array method) detaches it.

    RED FLAGS

    • Saying `this` refers to the object where the function was written.
    • Using arrow functions as object methods and expecting `this` to be the object.

    LIKELY FOLLOW-UPS

    • Why do class field arrow functions 'fix' handlers, and what do they cost?
    • What is `this` inside a top-level arrow function in an ES module?

    SOURCES

  4. Q4Mid

    How does prototypal inheritance work, and what do ES classes change about it?

    Reveal model answer

    Every object has an internal [[Prototype]] link; when a property is not found on the object, lookup walks up that chain until it reaches `null`. Constructor functions put shared methods on `Fn.prototype`, and `new` creates an object whose prototype is that object. Classes are syntax over the same mechanism: methods still live on the prototype and `extends` wires up two prototype chains, but classes add real features such as private `#fields`, mandatory `new`, strict mode and `super`.

    WHAT A STRONG ANSWER COVERS

    • Property lookup walks the prototype chain; writes create own properties.
    • Methods are shared via the prototype, instance data lives on the instance.
    • Classes do not introduce a new inheritance model; `typeof` a class is 'function'.
    • Classes add private elements, static blocks and enforce `new`.
    • Prefer composition over deep inheritance hierarchies.

    RED FLAGS

    • Claiming JavaScript classes copy methods into each instance like Java classes.
    • Confusing `obj.__proto__` with `Fn.prototype`.

    LIKELY FOLLOW-UPS

    • What does `Object.create(null)` give you and when is it useful?
    • How does `instanceof` decide its answer?

    SOURCES

  5. Q5Junior

    What is the difference between `==` and `===`? What do `[] == false`, `null == undefined` and `NaN === NaN` evaluate to?

    Reveal model answer

    `===` compares without type conversion; `==` applies coercion rules first, converting operands toward numbers or primitives. `[] == false` is true because both sides coerce to 0, `null == undefined` is true by a special rule (and neither is loosely equal to anything else), and `NaN === NaN` is false because NaN is never equal to itself. Use `===` by default, `Number.isNaN` for NaN, and `Object.is` when you need to distinguish -0 from +0.

    WHAT A STRONG ANSWER COVERS

    • `===` has no coercion; `==` coerces with non-obvious rules.
    • NaN is not equal to itself under either operator; `Object.is(NaN, NaN)` is true.
    • `+0 === -0` is true; `Object.is` says false.
    • `x == null` is the one widely accepted use of loose equality (null or undefined).
    • `includes`, Set and Map use SameValueZero, so they find NaN where `indexOf` does not.

    RED FLAGS

    • Saying `==` 'compares values' and `===` 'compares types' without mentioning coercion.
    • Thinking `===` compares objects by content.

    LIKELY FOLLOW-UPS

    • How would you compare two objects for structural equality?
    • Why does `[NaN].indexOf(NaN)` return -1 but `[NaN].includes(NaN)` return true?

    SOURCES

  6. Q6Junior

    Explain hoisting and the temporal dead zone. What happens with `console.log(a); console.log(b); var a = 1; let b = 2;`?

    Reveal model answer

    Declarations are processed before code in a scope runs. `var` bindings are created and initialised to `undefined`, so `console.log(a)` prints undefined. `let`, `const` and `class` bindings are created but left uninitialised until their declaration executes; touching them before that, even with `typeof`, throws a ReferenceError, so `console.log(b)` throws. Function declarations are hoisted with their body, function expressions are not.

    WHAT A STRONG ANSWER COVERS

    • `var` hoists as undefined; `let`/`const`/`class` are in the TDZ until the declaration runs.
    • The TDZ is temporal: a closure defined earlier can read the variable if called after initialisation.
    • Function declarations are fully hoisted; `const f = () => {}` is not callable before its line.
    • `typeof` on a TDZ variable throws, unlike on an undeclared name.

    RED FLAGS

    • Saying `let` and `const` are not hoisted at all.
    • Describing hoisting as the engine physically moving code to the top.

    LIKELY FOLLOW-UPS

    • Why does a `switch` with `let` in two `case` clauses throw a SyntaxError?
    • How do circular ES module imports interact with the TDZ?

    SOURCES

  7. Q7Mid

    What is wrong here? `async function loadUser(id) { try { return fetchUser(id); } catch (err) { return null; } }` And how do promise chains and async/await differ for error handling?

    Reveal model answer

    `return fetchUser(id)` returns the promise without awaiting it, so the function exits the try block before the promise settles and a rejection bypasses the catch; it must be `return await fetchUser(id)`. With async/await, an awaited rejection throws at the await, so ordinary try/catch/finally works; with raw promises you attach `.catch` and must return every inner promise so errors propagate. In both styles, a promise nobody awaits or catches becomes an unhandled rejection.

    WHAT A STRONG ANSWER COVERS

    • Inside try/catch, `return await` is needed for the catch to see the rejection.
    • An awaited rejection throws synchronously at the `await` point.
    • Promise chains must return inner promises, or errors and ordering are lost.
    • `array.forEach(async ...)` does not wait and does not propagate errors; use `for...of` with await or Promise.all over `map`.
    • Sequential awaits of independent work are slower than starting them together.

    RED FLAGS

    • Believing try/catch catches any error thrown later by a promise created inside it.
    • Adding `async` to a forEach callback and assuming the loop waits.
    • Swallowing errors with an empty catch.

    LIKELY FOLLOW-UPS

    • What happens in Node.js when a rejection is never handled?
    • How would you add a timeout to an awaited call?

    SOURCES

  8. Q8Mid

    Compare `Promise.all`, `Promise.allSettled`, `Promise.any` and `Promise.race`. When would you use each?

    Reveal model answer

    `all` fulfils with every value in input order and rejects on the first rejection; use it when you need all results. `allSettled` never rejects and gives a `{status, value | reason}` per input; use it for best-effort batches where partial failure is fine. `any` fulfils with the first fulfilment and rejects with an AggregateError only if all reject; use it for redundant sources. `race` settles like whichever input settles first, which is the classic way to add a timeout.

    WHAT A STRONG ANSWER COVERS

    • `all` fails fast but does not cancel the other operations; they keep running.
    • `allSettled` reports per-item outcomes and never rejects.
    • `any` ignores rejections until all fail, then throws AggregateError.
    • `race` settles on the first settlement, including a rejection.
    • All of them run work concurrently only because the promises already exist; unbounded fan-out needs a concurrency limit.

    RED FLAGS

    • Thinking `Promise.all` cancels the remaining requests on failure.
    • Mixing up `any` and `race`.
    • Firing thousands of requests through `Promise.all` without a limit.

    LIKELY FOLLOW-UPS

    • How would you cap concurrency at 5 for 1,000 requests?
    • How do you actually cancel the losers of a race (AbortController)?

    SOURCES

  9. Q9Junior

    What is the difference between debounce and throttle? Which would you use for a search box and which for a scroll handler?

    Reveal model answer

    Debounce waits until calls stop for a given interval and then runs once, so a burst collapses into a single call; that suits a search box, where you only want to query after the user pauses typing. Throttle runs at most once per interval while calls keep coming, guaranteeing regular updates; that suits scroll or resize handlers that need steady feedback. Both are implemented with a timer in a closure, and both may need leading or trailing edge options and cancellation on unmount.

    WHAT A STRONG ANSWER COVERS

    • Debounce: wait for quiet, then fire once (usually trailing edge).
    • Throttle: fire at most once per window during continuous activity.
    • Search input: debounce; scroll, resize, pointermove: throttle or requestAnimationFrame.
    • Clean up pending timers on unmount, and still guard against out-of-order responses.

    RED FLAGS

    • Using the two terms interchangeably.
    • Believing debounce alone prevents stale search results arriving out of order.

    LIKELY FOLLOW-UPS

    • Write a debounce function from memory.
    • Why might requestAnimationFrame be better than a throttle for visual updates?

    SOURCES

  10. Q10Junior

    What is the difference between a shallow and a deep copy? What are the trade-offs of spread, `JSON.parse(JSON.stringify(x))` and `structuredClone`?

    Reveal model answer

    A shallow copy duplicates only the top level, so nested objects are still shared and mutating them affects the original; spread and `Object.assign` are shallow. JSON round-tripping deep-copies plain data but drops undefined and functions, turns Dates into strings and Maps into empty objects, and throws on cycles. `structuredClone` deep-copies with the structured clone algorithm, preserving Dates, Maps, Sets, typed arrays and circular references, but throws a DataCloneError on functions and DOM nodes and does not preserve class prototypes.

    WHAT A STRONG ANSWER COVERS

    • Spread and Object.assign are shallow.
    • JSON round-trip loses types, undefined and functions, and fails on cycles.
    • structuredClone handles cycles and many built-in types.
    • structuredClone throws on functions and returns plain objects for class instances.
    • Often the better answer is immutable updates of only the changed path, not deep copying everything.

    RED FLAGS

    • Saying spread makes a deep copy.
    • Recommending JSON round-trip without knowing what it loses.

    LIKELY FOLLOW-UPS

    • Why does an immutable update matter for React state?
    • How is structuredClone related to postMessage?

    SOURCES

  11. Q11Senior

    How does garbage collection work in JavaScript, and what are common causes of memory leaks in long-lived apps?

    Reveal model answer

    Modern engines use tracing collection based on reachability: starting from roots such as globals and the live stack, anything reachable is kept and everything else is reclaimed, so cycles are not a problem. A leak is therefore memory that is still reachable but no longer useful. Common causes are event listeners, intervals and subscriptions never removed, unbounded caches or Maps keyed by objects, closures holding large objects, and detached DOM nodes still referenced from JS. You find them by comparing heap snapshots over time and following the retainer path.

    WHAT A STRONG ANSWER COVERS

    • GC is reachability-based (mark-and-sweep family); circular references are collected.
    • A leak is an unintended reference from a root, not 'memory GC forgot'.
    • Usual suspects: listeners, timers, subscriptions, unbounded caches, closures, detached DOM.
    • WeakMap or WeakRef let caches avoid keeping keys alive; bounded LRU caches cap growth.
    • Diagnose with heap snapshot comparison and retainer chains, not guesswork.

    RED FLAGS

    • Saying reference cycles leak in modern engines.
    • Fixing leaks by raising the heap size.
    • Calling `delete` or setting variables to null everywhere as a general practice.

    LIKELY FOLLOW-UPS

    • Why are WeakMaps not iterable?
    • How would you prove a leak is fixed?

    SOURCES

  12. Q12Senior

    What are the differences between ES modules and CommonJS, and how do they interoperate in current Node.js?

    Reveal model answer

    ES modules use static `import`/`export` that is resolved before execution, support top-level await, are always strict, and export live read-only bindings; CommonJS uses a synchronous runtime `require` that returns the `module.exports` object. ESM can import CommonJS (module.exports becomes the default export). Current Node.js lines can also `require()` an ES module as long as it has no top-level await; otherwise CommonJS must use dynamic `import()`. Node decides the format from `.mjs`/`.cjs`, the nearest package.json `type` field, or syntax detection.

    WHAT A STRONG ANSWER COVERS

    • ESM is statically analysable, which enables tree shaking and early errors.
    • ESM exports are live bindings; CJS gives you whatever was on module.exports.
    • require(esm) works for synchronous module graphs; top-level await throws ERR_REQUIRE_ASYNC_MODULE.
    • `type: module`, `.mjs` and `.cjs` control interpretation; `exports` in package.json controls entry points.
    • ESM has no `__dirname`/`require` by default; use `import.meta.dirname` or `import.meta.url`.

    RED FLAGS

    • Saying CommonJS can never load ES modules.
    • Thinking import/export is just syntax sugar for require.
    • Shipping a dual package without considering two copies of module state.

    LIKELY FOLLOW-UPS

    • What is the dual package hazard?
    • Why does a bundler tree-shake ESM more reliably than CJS?

    SOURCES