Skip to content

JavaScript

What experienced JavaScript engineers forget before an interview, grouped by subtopic.

Event loop

Microtasks vs macrotasks

Rendering in the loop

  • requestAnimationFrame callbacks, style, layout, and paint run between tasks, at most once per frame (~16.7 ms at 60 Hz), after microtasks drain.
  • A long task or microtask chain delays the next frame — this is what INP measures (see frontend.md).

Scope and closures

Hoisting and the TDZ

  • var is hoisted and reads as undefined before its line; a function declaration is callable before its line.
  • let/const/class are hoisted but sit in the temporal dead zone — access before the declaration throws ReferenceError.

Closures in loops

  • for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) logs 3,3,3 — one function-scoped i shared by every closure.
  • let creates a fresh binding per iteration, printing 0,1,2.

this and prototypes

this binding rules

  • Precedence: new > explicit call/apply/bind > implicit (obj.method()) > default (undefined in strict mode, global otherwise).
  • Arrow functions have no own this; they capture it lexically.
  • Passing obj.method as a bare callback (setTimeout(obj.method)) loses this.

Prototypes and classes

  • Property lookup walks the [[Prototype]] chain until found or null; class methods live on C.prototype.
  • Unlike constructor functions, classes run in strict mode, have a TDZ, and throw if called without new.
  • #private fields are engine-enforced: obj.#x outside the class is a syntax error.

Coercion and equality

==, ===, Object.is

  • == coerces: '' == 0 and null == undefined are true, but null == 0 is false; === never coerces.
  • NaN !== NaN; Object.is treats NaN as equal to itself and distinguishes +0 from -0.

Type-check quirks

Numbers

IEEE 754 doubles

  • Every number is a 64-bit double: 0.1 + 0.2 !== 0.3; compare with a tolerance or use integers (cents).
  • Integers are exact only up to Number.MAX_SAFE_INTEGER = 2⁵³ − 1; IDs beyond that (Snowflake, tweet IDs) must travel as strings or BigInt.
  • Bitwise operators on numbers truncate to 32-bit signed integers (>>> returns unsigned); on BigInt they are arbitrary-precision.
  • Math.sumPrecise(iterable) (ES2026) adds floats without accumulating rounding error, unlike a naive reduce.

Typed arrays

Async

Promise combinators

Combinator Settles when Use when
Promise.all any rejects, or all fulfill need every result, fail fast
Promise.allSettled all settle (input rejections don't reject it), tagged fulfilled/rejected need every outcome
Promise.race first settles (either way) timeouts, first response wins
Promise.any first fulfills, or all reject (AggregateError) first success wins

async/await ordering

  • The new Promise executor runs synchronously; only the .then callbacks are deferred.
  • Code after await always resumes as a microtask, even when the awaited value is already resolved or isn't a promise.
  • Inside try, return promise skips the catch when it rejects; write return await promise.
  • An async function always returns a new promise, so a throw inside it becomes a rejection, not a synchronous exception.

Sequential vs parallel await

  • for (const x of xs) await f(x) runs serially: each call starts after the previous settles; start all calls first, then await Promise.all([...]) so they overlap.
  • An unhandled rejection doesn't throw synchronously; Node crashes the process on it by default since Node 15.

Cancellation and async iteration

fetch semantics

  • fetch rejects only when the request can't be made (network error, CORS, invalid URL or options, abort); HTTP 4xx/5xx resolve with res.ok === false, so check it.
  • The body is a stream and can be read once; call res.clone() before reading it twice.
  • There is no default timeout: pass signal: AbortSignal.timeout(ms).
  • Cookies go only to the same origin by default (credentials: 'same-origin'); cross-origin requests need 'include' plus CORS credential headers.

Modules

CommonJS vs ESM

  • CommonJS require loads synchronously at runtime and can be called conditionally; a destructured import is a snapshot, not a live binding.
  • ESM is statically analyzed — enabling tree shaking and live bindings — loads asynchronously, and allows top-level await.

Node interop and the dual-package hazard

  • require(esm) works for synchronous ES modules (no top-level await) without a flag since Node 22.12 / 20.19.
  • Dual-package hazard: a package shipping CJS and ESM builds can load twice under different identities, breaking instanceof and singletons.

Memory

Garbage collection and leaks

  • V8's collector is generational: a fast scavenger for the young generation (most objects die young), mark-compact for the old generation.
  • Common leaks: detached DOM nodes still referenced from JS, forgotten listeners and timers, unbounded caches.
  • A closure keeps its referenced outer variables alive, so a long-lived listener can retain a large object it barely uses.

Hidden classes and inline caches

  • V8 gives objects created with the same properties in the same order a shared hidden class (shape); adding a property transitions to a new one.
  • Each property-access site caches the shapes it has seen: monomorphic (one) is fastest, polymorphic up to 4, then megamorphic (a generic slow lookup).
  • Initialize every field in the constructor in a fixed order; delete on a hot object can drop it into slow dictionary mode.
  • Arrays track element kinds too: mixing integers, doubles, and holes (new Array(n)) downgrades them for good — except that Array.prototype.fill can repack a holey array (since February 2025).

Weak references

  • WeakMap/WeakSet keys don't keep objects alive — good for per-object metadata and caches.
  • WeakRef weakly holds one object; FinalizationRegistry callbacks may run late or never — cleanup hints only, never correctness.

Deep cloning

  • structuredClone handles cycles, Map, Set, Date, and typed arrays, but not functions or DOM nodes.
  • JSON.parse(JSON.stringify(x)) (JSON.stringify) drops undefined/functions/symbols, turns Date into a string, throws on BigInt and cycles.

Metaprogramming and iteration

Proxy and Reflect

  • A Proxy intercepts operations (get, set, has, delete, apply) on a target — the basis of Vue's reactivity and MobX.
  • Reflect exposes the default behavior of each trap, so handlers can forward with Reflect.get(target, key, receiver).

Iterators and generators

  • An object is iterable if it has [Symbol.iterator]() returning { next() → { value, done } }; for...of, spread, and destructuring use it.
  • Generators (function*) are lazy, pause at yield, and can receive values via next(v); async generators power for await.
  • Iterator helpers (.map, .filter, .take, .drop on iterators, ES2025) are lazy, unlike the array methods.

Node.js runtime

Node event loop phases

  • Phases: timers → pending callbacks → poll (I/O) → check (setImmediate) → close.
  • Since Node 20 (libuv 1.45), timers run after poll in each iteration instead of before it (plus once when the loop starts); this can shift timer vs setImmediate ordering.
  • After each callback, the process.nextTick queue drains first, then promise microtasks.
  • setTimeout(f, 0) vs setImmediate(f) order is nondeterministic in the main module; inside an I/O callback setImmediate always runs first.

libuv thread pool

Stream backpressure

Worker threads and cluster

  • worker_threads: threads in one process with separate V8 isolates, for CPU-bound JS; share memory through SharedArrayBuffer.
  • cluster: forks processes sharing one server port to use all cores — in containers, usually replaced by more replicas.

AsyncLocalStorage

  • AsyncLocalStorage carries request-scoped context (request ID, trace, user) through every async call without passing it as an argument.
  • The basis of Node APM tracing (e.g. OpenTelemetry) and per-request logging.

DOM events

Propagation and delegation

Listener performance

  • passive: true promises not to call preventDefault(), so scrolling starts without waiting for the handler.
  • Debounce fires once after a pause (search-as-you-type); throttle fires at most once per interval (scroll, resize).

Recent language additions

Temporal

  • Temporal (Stage 4 in March 2026, ES2027) replaces Date with immutable values and separate types for instants, dates, times, and zoned date-times.
  • Time zones are explicit (ZonedDateTime) and arithmetic is calendar-safe, unlike Date's mutable, local-time-by-default API.

Explicit resource management