JavaScript¶
What experienced JavaScript engineers forget before an interview, grouped by subtopic.
Event loop¶
Microtasks vs macrotasks¶
- The microtask queue (promise reactions,
queueMicrotask,MutationObserver) drains completely whenever the call stack empties. - The task (macrotask) queue (
setTimeout, I/O, UI events) runs one task per loop iteration, then microtasks drain again. - So
Promise.resolve().then(f)always beatssetTimeout(f, 0); a microtask that keeps queuing microtasks starves rendering and tasks.
Rendering in the loop¶
requestAnimationFramecallbacks, 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¶
varis hoisted and reads asundefinedbefore its line; afunctiondeclaration is callable before its line.let/const/classare hoisted but sit in the temporal dead zone — access before the declaration throwsReferenceError.
Closures in loops¶
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i))logs3,3,3— one function-scopedishared by every closure.letcreates a fresh binding per iteration, printing0,1,2.
this and prototypes¶
this binding rules¶
- Precedence:
new> explicitcall/apply/bind> implicit (obj.method()) > default (undefinedin strict mode, global otherwise). - Arrow functions have no own
this; they capture it lexically. - Passing
obj.methodas a bare callback (setTimeout(obj.method)) losesthis.
Prototypes and classes¶
- Property lookup walks the
[[Prototype]]chain until found ornull;classmethods live onC.prototype. - Unlike constructor functions, classes run in strict mode, have a TDZ, and throw if called without
new. #privatefields are engine-enforced:obj.#xoutside the class is a syntax error.
Coercion and equality¶
==, ===, Object.is¶
==coerces:'' == 0andnull == undefinedaretrue, butnull == 0isfalse;===never coerces.NaN !== NaN;Object.istreatsNaNas equal to itself and distinguishes+0from-0.
Type-check quirks¶
typeof null === 'object'— a historical bug kept for compatibility.- Default
Array.prototype.sort()compares as strings:[10, 2, 1].sort()→[1, 10, 2]; pass(a, b) => a - b.
Numbers¶
IEEE 754 doubles¶
- Every
numberis 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 orBigInt. - Bitwise operators on numbers truncate to 32-bit signed integers (
>>>returns unsigned); onBigIntthey are arbitrary-precision. Math.sumPrecise(iterable)(ES2026) adds floats without accumulating rounding error, unlike a naivereduce.
Typed arrays¶
- Typed arrays are fixed-type views over an
ArrayBuffer— unboxed numeric storage; several views can share one buffer. SharedArrayBuffer+Atomicsshare memory between workers.
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 Promiseexecutor runs synchronously; only the.thencallbacks are deferred. - Code after
awaitalways resumes as a microtask, even when the awaited value is already resolved or isn't a promise. - Inside
try,return promiseskips thecatchwhen it rejects; writereturn await promise. - An
asyncfunction always returns a new promise, so athrowinside 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, thenawait 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¶
AbortControllercancelsfetchand other abortable APIs via a sharedAbortSignal;AbortSignal.timeout(ms)builds a deadline.for await...ofconsumes async generators and any object withSymbol.asyncIterator.
fetch semantics¶
fetchrejects only when the request can't be made (network error, CORS, invalid URL or options, abort); HTTP 4xx/5xx resolve withres.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
requireloads 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-levelawait) 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
instanceofand 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;
deleteon 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 thatArray.prototype.fillcan repack a holey array (since February 2025).
Weak references¶
WeakMap/WeakSetkeys don't keep objects alive — good for per-object metadata and caches.WeakRefweakly holds one object;FinalizationRegistrycallbacks may run late or never — cleanup hints only, never correctness.
Deep cloning¶
structuredClonehandles cycles,Map,Set,Date, and typed arrays, but not functions or DOM nodes.JSON.parse(JSON.stringify(x))(JSON.stringify) dropsundefined/functions/symbols, turnsDateinto a string, throws onBigIntand cycles.
Metaprogramming and iteration¶
Proxy and Reflect¶
- A
Proxyintercepts operations (get, set, has, delete, apply) on a target — the basis of Vue's reactivity and MobX. Reflectexposes the default behavior of each trap, so handlers can forward withReflect.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 atyield, and can receive values vianext(v); async generators powerfor await. - Iterator helpers (
.map,.filter,.take,.dropon 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
setImmediateordering. - After each callback, the
process.nextTickqueue drains first, then promise microtasks. setTimeout(f, 0)vssetImmediate(f)order is nondeterministic in the main module; inside an I/O callbacksetImmediatealways runs first.
libuv thread pool¶
- Network sockets use epoll/kqueue, but
fs,dns.lookup, asynccrypto(pbkdf2,scrypt), andzlibrun on the libuv thread pool. - The pool has 4 threads by default (
UV_THREADPOOL_SIZE), so a few slow DNS lookups or hashes can stall all file I/O.
Stream backpressure¶
write()returnsfalseonce the buffer passeshighWaterMark; stop writing until the'drain'event.stream.pipeline()handles backpressure, errors, and cleanup of every stream in the chain —.pipe()doesn't forward errors.
Worker threads and cluster¶
worker_threads: threads in one process with separate V8 isolates, for CPU-bound JS; share memory throughSharedArrayBuffer.cluster: forks processes sharing one server port to use all cores — in containers, usually replaced by more replicas.
AsyncLocalStorage¶
AsyncLocalStoragecarries 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¶
- Phases: capture (root → target) → target → bubble (target → root);
{capture: true}listens during capture. - Event delegation: one listener on an ancestor plus
event.targetcovers many children, including ones added later.
Listener performance¶
passive: truepromises not to callpreventDefault(), 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) replacesDatewith immutable values and separate types for instants, dates, times, and zoned date-times.- Time zones are explicit (
ZonedDateTime) and arithmetic is calendar-safe, unlikeDate's mutable, local-time-by-default API.
Explicit resource management¶
using/await using(ES2027; TypeScript since 5.2) call[Symbol.dispose]()/[Symbol.asyncDispose]()when the block exits, even on throw — notry/finally.DisposableStackcollects several resources and disposes them in reverse order.