Back to interview questions

100+ JavaScript Interview Questions and Answers

100+ JavaScript Interview Questions and Answers

Explore 120 JavaScript interview questions and answers on types, closures, objects, promises, browser APIs, Node.js, testing, performance, security, and design.

Explore 120 JavaScript interview questions and answers on types, closures, objects, promises, browser APIs, Node.js, testing, performance, security, and design.

By

Team Sherlock.sh

Add Sherlock as a preferred source on Google

JavaScript interviews should reveal whether a candidate can explain observable behavior, manage asynchronous work, and separate language rules from runtime APIs. Recruiters can use a small code change, an explicit execution environment, and a follow-up about failure handling to distinguish memorized output puzzles from practical engineering judgment.

This guide to 100+ JavaScript Interview Questions and Answers contains 120 questions across beginner, intermediate, and experienced levels. It moves from values, scope, functions, and objects to asynchronous execution, browser integration, Node.js, testing, performance, security, and production design without assuming a particular UI framework.

How can recruiters identify potential AI cheating, deepfakes, impersonation, and unauthorized assistance during JavaScript interviews?

Ask candidates to explain why a closure retains state, repair a promise error path, or distinguish a browser task from a language-level promise reaction. Fluent terminology alone does not establish independent understanding of JavaScript behavior.

Sherlock AI helps recruiters detect potential AI-assisted cheating, deepfakes, candidate impersonation, and unauthorized real-time assistance during JavaScript interviews.

Combine integrity signals with practical code review and role-relevant follow-up questions. Evaluate signals in context rather than treating any one signal as proof of misconduct.

JavaScript Interview Questions

Select questions appropriate to the role, and state whether a snippet runs as a script, an ECMAScript module, or Node.js CommonJS. Specify the runtime before asking about host APIs or scheduling.

JavaScript Interview Questions for Beginners

Establish precise understanding of values, scope, functions, collections, and everyday syntax.

  1. What is JavaScript, and how does the language differ from its runtime?

  2. What are JavaScript’s primitive types, and how do objects differ?

  3. How do typeof, Array.isArray(), and null checks help identify values?

  4. What is the difference between null, undefined, and a missing property?

  5. What is NaN, and how should numeric input be checked?

  6. When should you use explicit conversion instead of implicit coercion?

  7. How do strict equality, loose equality, and Object.is() differ?

  8. Which values are falsy, and why can truthiness checks lose valid data?

  9. How do var, let, and const differ?

  10. What does hoisting mean, and what is the temporal dead zone?

  11. How does lexical scope determine which variable a function reads?

  12. Does const make an object immutable?

  13. How do function declarations and function expressions differ?

  14. When is an arrow function different from an ordinary function?

  15. How is this determined for a regular function call?

  16. What is a closure, and how can it preserve private state?

  17. Are JavaScript arguments passed by value or by reference?

  18. How do default parameters work with undefined and null?

  19. How do rest parameters differ from spread syntax?

  20. How does destructuring make data access clearer?

  21. When should you use for...of instead of for...in?

  22. How do map() and forEach() differ?

  23. How do filter(), find(), some(), and every() differ?

  24. How do you use reduce() safely, including for an empty array?

  25. Which common array operations mutate their input?

  26. How should numeric arrays be sorted?

  27. How do slice() and splice() differ?

  28. How do you distinguish an own property from an inherited one?

  29. What does a shallow object or array copy actually copy?

  30. How do optional chaining and nullish coalescing work together?

  31. How do template literals differ from quoted strings?

  32. Why can string length differ from the number of visible characters?

  33. What are the limits of Number for decimals and large integers?

  34. When should you use BigInt, and what are its constraints?

  35. What can JSON represent, and why is it not a universal cloning tool?

  36. How should try, catch, finally, and throw be used?

  37. What changes when JavaScript runs in strict mode?

  38. How do named and default exports work in ES modules?

  39. What are callbacks and higher-order functions?

  40. What makes a function pure, and why does that help testing?

JavaScript Interview Questions for Intermediate

Explore object mechanics, modules, asynchronous control flow, and browser integration.

  1. How does prototype-chain property lookup work?

  2. How do classes relate to prototypes in JavaScript?

  3. How do extends, super, and derived constructors work?

  4. What do private class fields protect?

  5. How do static methods differ from instance methods?

  6. How do call, apply, and bind control this?

  7. Why does an arrow callback preserve its surrounding this?

  8. What are property descriptors and their default attributes?

  9. When should getters and setters be used?

  10. Which properties do Object.keys and Reflect.ownKeys return?

  11. What are symbols useful for, and are they private?

  12. When should you use Map instead of an object?

  13. How does Set determine whether values are duplicates?

  14. What does WeakMap offer that Map does not?

  15. How do shallow copies differ from structured clones?

  16. How does the iterable protocol differ from the iterator protocol?

  17. How do generators pause and resume execution?

  18. How can iterator cleanup run when a loop exits early?

  19. How do per-iteration let bindings differ from a shared var binding in closures?

  20. What are ES module live bindings?

  21. Why can circular module dependencies fail during initialization?

  22. When should you use dynamic import?

  23. What does resolving a promise actually mean?

  24. How do returned values and thrown errors affect promise chains?

  25. How does Promise.all differ from sequential awaiting?

  26. When should you choose Promise.allSettled, any, or race?

  27. How does async/await change control flow and error handling?

  28. Why is async forEach usually wrong for awaited work?

  29. How do tasks and microtasks interact in a browser event loop?

  30. Why is a zero-delay timer not immediate?

  31. What does AbortController cancel, and what does it not cancel?

  32. How should fetch handle HTTP errors and response bodies?

  33. How do event capture, targeting, and bubbling work?

  34. How do preventDefault and propagation controls differ?

  35. How can event delegation handle dynamically added elements?

  36. How do you remove event listeners reliably?

  37. How do DOM attributes differ from element properties?

  38. How do live DOM collections differ from static query results?

  39. When should DOM work use requestAnimationFrame?

  40. How do async iterables differ from ordinary iterables?

JavaScript Interview Questions for Experienced

Assess production decisions about concurrency, resource ownership, testing, security, and performance.

  1. How would you limit concurrent asynchronous operations without changing result order?

  2. How do you prevent stale asynchronous results from overwriting newer state?

  3. What makes a retry policy safe rather than merely persistent?

  4. Why can race conditions occur in single-threaded JavaScript?

  5. How should cancellation propagate through an asynchronous workflow?

  6. How does backpressure prevent an asynchronous pipeline from exhausting memory?

  7. How would you design an asynchronous iterator with reliable cleanup?

  8. What changes when reasoning about the Node.js event loop instead of a browser event loop?

  9. When should CPU-heavy JavaScript move to a worker?

  10. How would you process large data with Node.js streams?

  11. How do you investigate a JavaScript memory leak?

  12. Why are WeakRef and FinalizationRegistry unsuitable for essential cleanup?

  13. How would you build a bounded cache rather than an accidental memory leak?

  14. How do you profile JavaScript before optimizing it?

  15. How can a change in data structure improve algorithmic performance?

  16. How do debounce and throttle differ in a production interface?

  17. Why can repeated microtasks make a browser interface unresponsive?

  18. How would you avoid layout thrashing in browser code?

  19. How do you test asynchronous code without relying on arbitrary delays?

  20. What are the limits of fake timers in JavaScript tests?

  21. How does dependency injection improve testability without requiring a framework?

  22. What properties would you test beyond a few example inputs?

  23. How do you prevent DOM-based cross-site scripting?

  24. How do you prevent prototype pollution when accepting dynamic keys?

  25. Why should configuration data not be executed with eval or Function?

  26. Where should validation and authorization occur in a JavaScript system?

  27. How should a module expose a small, stable public API?

  28. What does immutable state mean when JavaScript objects are nested?

  29. How should a JavaScript API represent recoverable failures?

  30. How do you design a serialization boundary that survives schema changes?

  31. When should you use typed arrays and DataView?

  32. How do structured cloning and transferable objects differ?

  33. When are SharedArrayBuffer and Atomics appropriate?

  34. How should browser storage choices reflect consistency and security requirements?

  35. How do you design event subscriptions so components can be disposed safely?

  36. How do you avoid duplicate work with an in-flight promise cache?

  37. How should timeout handling differ from cancellation?

  38. How do you decide between sequential, concurrent, and parallel execution?

  39. How do you preserve useful error context without leaking sensitive information?

  40. How would you review a JavaScript service for production readiness?

JavaScript Interview Questions and Answers

The answers follow the question bank exactly and lead with responses candidates can adapt. Language examples use a stable ECMAScript 2024 baseline unless explicitly qualified; browser APIs and Node.js APIs are identified separately. Node.js examples target Node 22 or later, and safe local fixtures were checked with Node v24.22.1. This is a compatibility baseline, not a claim that ECMAScript 2024 or Node 22 is the latest release.

JavaScript Interview Questions and Answers for Beginners

These answers focus on fundamental behavior, explicit assumptions, and useful small examples.

1. What is JavaScript, and how does the language differ from its runtime?

JavaScript is a dynamically typed, garbage-collected programming language whose core behavior is standardized as ECMAScript. A runtime combines a language engine with host capabilities. Arrays, functions, objects, and promises are language features; DOM elements and browser navigation belong to browser APIs. Node.js supplies server-side facilities such as filesystem access, although modern runtimes share some web-compatible APIs.

I separate these layers before choosing an implementation or explaining an example. A function that sums an array can run in either environment, but code using document.querySelector() requires a DOM. Neither a browser’s event loop nor Node’s module-loading configuration can be inferred solely from JavaScript syntax. The examples here use established ECMAScript 2024 features unless a host is identified.

2. What are JavaScript’s primitive types, and how do objects differ?

JavaScript has seven primitive types: undefined, null, boolean, number, bigint, string, and symbol. Objects are a separate type; arrays and callable functions are objects. Primitives are immutable values, while an object can hold mutable properties and has identity. Two separately created objects are not equal merely because their contents match.

JavaScript example

console.log(3 === 3);       // true
console.log({id: 3} === {id: 3}); // false
const first = {id: 3};
const second = first;
console.log(first === second);  // true

console.log(3 === 3);       // true
console.log({id: 3} === {id: 3}); // false
const first = {id: 3};
const second = first;
console.log(first === second);  // true

console.log(3 === 3);       // true
console.log({id: 3} === {id: 3}); // false
const first = {id: 3};
const second = first;
console.log(first === second);  // true

Variables are not permanently assigned a primitive type: a let binding can later hold a different type. That flexibility does not remove the need to validate inputs. Wrapper objects such as new Boolean(false) are generally inappropriate because their object behavior differs from the corresponding primitive.

3. How do typeof, Array.isArray(), and null checks help identify values?

typeof provides a quick classification, but it is not a complete type validator. It returns "object" for both arrays and null, and "function" for callable functions. I check null explicitly and use Array.isArray() when an array is required. For domain data, I then validate the individual fields rather than trusting one tag.

JavaScript example

function classify(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  return typeof value;
}
console.log([null, [], {}, 2n].map(classify));
// ["null", "array", "object", "bigint"]

function classify(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  return typeof value;
}
console.log([null, [], {}, 2n].map(classify));
// ["null", "array", "object", "bigint"]

function classify(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  return typeof value;
}
console.log([null, [], {}, 2n].map(classify));
// ["null", "array", "object", "bigint"]

typeof missingName can return "undefined" for an undeclared identifier, but accessing a lexical binding in its temporal dead zone still throws. Checking whether a value is an object also does not prove it is a plain record, safe to inspect, or free of getters.

4. What is the difference between null, undefined, and a missing property?

undefined commonly means no value was supplied or produced; null is an explicit value often used to represent an intentional absence. A missing property and an own property whose value is undefined both read as undefined, but their structures differ. I distinguish them when implementing patch APIs, validation, or serialization rules.

JavaScript example

const settings = {theme: undefined, avatar: null};
console.log(settings.theme, settings.missing); // undefined undefined
console.log(Object.hasOwn(settings, "theme")); // true
console.log(Object.hasOwn(settings, "missing")); // false

const settings = {theme: undefined, avatar: null};
console.log(settings.theme, settings.missing); // undefined undefined
console.log(Object.hasOwn(settings, "theme")); // true
console.log(Object.hasOwn(settings, "missing")); // false

const settings = {theme: undefined, avatar: null};
console.log(settings.theme, settings.missing); // undefined undefined
console.log(Object.hasOwn(settings, "theme")); // true
console.log(Object.hasOwn(settings, "missing")); // false

The application must define what absence means. For example, an omitted patch field might mean “leave unchanged,” while null means “clear it.” Assigning undefined is not the same operation as deleting a property. Default parameters and destructuring defaults react to undefined, not to null.

5. What is NaN, and how should numeric input be checked?

NaN is a Number value representing an invalid numeric result, such as converting a nonnumeric string to Number. It is not equal to itself under ===. Number.isNaN() identifies NaN without first coercing its argument; the global isNaN() function does coerce. For a finite-number contract, Number.isFinite() is usually the more useful final check.

JavaScript example

function parsePrice(text) {
  if (typeof text !== "string" || text.trim() === "") return null;
  const value = Number(text);
  return Number.isFinite(value) && value >= 0 ? value : null;
}
console.log(parsePrice("12.5"), parsePrice(""), parsePrice("bad"));
// 12.5 null null

function parsePrice(text) {
  if (typeof text !== "string" || text.trim() === "") return null;
  const value = Number(text);
  return Number.isFinite(value) && value >= 0 ? value : null;
}
console.log(parsePrice("12.5"), parsePrice(""), parsePrice("bad"));
// 12.5 null null

function parsePrice(text) {
  if (typeof text !== "string" || text.trim() === "") return null;
  const value = Number(text);
  return Number.isFinite(value) && value >= 0 ? value : null;
}
console.log(parsePrice("12.5"), parsePrice(""), parsePrice("bad"));
// 12.5 null null

This accepts Number’s numeric-string grammar, not a locale-specific currency grammar. Blank strings need separate treatment because Number converts them to zero. Range, precision, and allowed formatting remain domain decisions.

6. When should you use explicit conversion instead of implicit coercion?

Explicit conversion makes an input boundary’s intent clearer than relying on an operator’s coercion. I use Number(value), String(value), or a deliberately defined parser, followed by validation. The + operator can concatenate strings, while arithmetic such as subtraction generally requests numeric conversion. A value’s origin therefore matters when reading seemingly simple expressions.

JavaScript example

const quantityText = "4";
console.log(quantityText + 1);         // "41"
console.log(Number(quantityText) + 1); // 5
console.log(Boolean("false"));         // true

const quantityText = "4";
console.log(quantityText + 1);         // "41"
console.log(Number(quantityText) + 1); // 5
console.log(Boolean("false"));         // true

const quantityText = "4";
console.log(quantityText + 1);         // "41"
console.log(Number(quantityText) + 1); // 5
console.log(Boolean("false"));         // true

Boolean() checks truthiness; it does not parse human-readable boolean words. Similarly, parseInt("12px", 10) accepts a numeric prefix, whereas Number("12px") produces NaN. I choose that behavior consciously rather than using either function as a universal validator. Object coercion can execute conversion hooks, so untrusted object input deserves additional care.

7. How do strict equality, loose equality, and Object.is() differ?

=== compares without cross-type coercion and is my normal choice for application equality. == applies specified conversions, which can be useful in a deliberately narrow check but can hide data inconsistencies. Object.is() resembles strict equality except that it treats NaN as equal to itself and distinguishes positive zero from negative zero. None of these recursively compares object contents.

JavaScript example

console.log(0 == "0", 0 === "0"); // true false
console.log(NaN === NaN, Object.is(NaN, NaN)); // false true
console.log(0 === -0, Object.is(0, -0)); // true false

console.log(0 == "0", 0 === "0"); // true false
console.log(NaN === NaN, Object.is(NaN, NaN)); // false true
console.log(0 === -0, Object.is(0, -0)); // true false

console.log(0 == "0", 0 === "0"); // true false
console.log(NaN === NaN, Object.is(NaN, NaN)); // false true
console.log(0 === -0, Object.is(0, -0)); // true false

value == null intentionally matches null and undefined for ordinary values, but explicit checks can communicate that intent more clearly. Collections have their own specified equality behavior: for example, includes() can find NaN. A business-level comparison still needs a definition of which fields and normalization rules count.

8. Which values are falsy, and why can truthiness checks lose valid data?

The ordinary falsy values are false, undefined, null, positive or negative zero, NaN, 0n, and the empty string. Nonempty strings, empty arrays, and empty objects are truthy. Browsers also retain the unusual legacy document.all exception; I do not use it as a model for normal objects. Truthiness is useful for conditional logic but is often too broad for checking whether user data exists.

JavaScript example

const retries = 0;
console.log(retries || 3); // 3: replaces a valid zero
console.log(retries ?? 3); // 0: replaces only null or undefined
console.log(Boolean([]), Boolean("0")); // true true

const retries = 0;
console.log(retries || 3); // 3: replaces a valid zero
console.log(retries ?? 3); // 0: replaces only null or undefined
console.log(Boolean([]), Boolean("0")); // true true

const retries = 0;
console.log(retries || 3); // 3: replaces a valid zero
console.log(retries ?? 3); // 0: replaces only null or undefined
console.log(Boolean([]), Boolean("0")); // true true

I test the actual contract: an array’s length for nonemptiness, value !== undefined for a supplied value, or explicit validation for numeric data. This prevents valid zero, false, and empty-string values from being accidentally overwritten.

9. How do var, let, and const differ?

let and const create block-scoped lexical bindings. let allows reassignment, while const requires an initializer and prevents later reassignment of that binding. var is scoped to its containing function, or to its script environment when declared outside a function; an ordinary block does not contain it. Repeated var declarations are permitted in situations where redeclaring a lexical binding would be an error.

JavaScript example

function example() {
  if (true) { var visible = 1; let local = 2; }
  return visible;
}
console.log(example()); // 1

function example() {
  if (true) { var visible = 1; let local = 2; }
  return visible;
}
console.log(example()); // 1

function example() {
  if (true) { var visible = 1; let local = 2; }
  return visible;
}
console.log(example()); // 1

I default to const, use let when reassignment expresses the algorithm, and avoid var in new code. This is about predictable scope, not a claim that const makes values immutable. Top-level behavior also depends on whether code runs as a browser script, an ES module, or a Node CommonJS module.

10. What does hoisting mean, and what is the temporal dead zone?

Hoisting is an informal description of declaration-processing behavior, not a source-code transformation that literally moves lines. A function declaration can generally be called before its declaration in the applicable scope. A var binding is initialized to undefined before execution reaches its initializer. A let, const, or class binding exists in its scope but cannot be accessed before initialization; that interval is the temporal dead zone.

JavaScript example

function demonstrate() {
  console.log(total); // undefined, not 7
  var total = 7;
  try { console.log(count); }
  catch (error) { console.log(error.name); } // ReferenceError
  let count = 2;
}
demonstrate();

function demonstrate() {
  console.log(total); // undefined, not 7
  var total = 7;
  try { console.log(count); }
  catch (error) { console.log(error.name); } // ReferenceError
  let count = 2;
}
demonstrate();

function demonstrate() {
  console.log(total); // undefined, not 7
  var total = 7;
  try { console.log(count); }
  catch (error) { console.log(error.name); } // ReferenceError
  let count = 2;
}
demonstrate();

The initializer still runs at its original execution point. I avoid explaining all declarations as equivalent “hoisted variables,” and I avoid legacy block-function behavior when teaching this distinction.

11. How does lexical scope determine which variable a function reads?

Lexical scope means name lookup follows where functions and blocks are written, not the chain of callers at runtime. A function retains access to its enclosing lexical environment. A nearer binding shadows an outer binding with the same name, and that nearer binding’s temporal dead zone also applies throughout its scope before initialization.

JavaScript example

const label = "outer";
function readLabel() { return label; }
function caller() {
  const label = "caller";
  return readLabel();
}
console.log(caller()); // "outer"

const label = "outer";
function readLabel() { return label; }
function caller() {
  const label = "caller";
  return readLabel();
}
console.log(caller()); // "outer"

const label = "outer";
function readLabel() { return label; }
function caller() {
  const label = "caller";
  return readLabel();
}
console.log(caller()); // "outer"

I use this rule to explain closures and to keep dependencies understandable. Renaming a local variable can prevent accidental shadowing, but passing a dependency explicitly is often clearer than having many functions silently read outer state. Lexical variable lookup is separate from the call-dependent this rules for regular functions.

12. Does const make an object immutable?

No. Const prevents assigning another value to the binding; it does not freeze an object reachable through that binding. The object’s properties can still change, and arrays can still gain elements. Immutability requires a separate design, such as copying on update, enforcing ownership, or freezing appropriately selected objects.

JavaScript example

const user = {name: "Mina", preferences: {theme: "light"}};
user.name = "Lee"; // permitted
Object.freeze(user);
user.preferences.theme = "dark"; // nested object is still mutable
console.log(user.name, user.preferences.theme); // "Lee" "dark"

const user = {name: "Mina", preferences: {theme: "light"}};
user.name = "Lee"; // permitted
Object.freeze(user);
user.preferences.theme = "dark"; // nested object is still mutable
console.log(user.name, user.preferences.theme); // "Lee" "dark"

const user = {name: "Mina", preferences: {theme: "light"}};
user.name = "Lee"; // permitted
Object.freeze(user);
user.preferences.theme = "dark"; // nested object is still mutable
console.log(user.name, user.preferences.theme); // "Lee" "dark"

Object.freeze() is shallow and does not make every conceivable object state immutable. Assignment to a frozen ordinary data property throws in strict mode and can fail silently in non-strict code. I avoid treating freezing as a security boundary, particularly for accessors or objects with specialized internal state.

13. How do function declarations and function expressions differ?

A function declaration introduces a named function binding through declaration processing. A function expression produces a function value when the expression is evaluated, commonly assigning that value to a variable or passing it as an argument. The variable’s own declaration rules determine whether it can be read before the assignment. A named function expression’s name is available inside that function, which helps recursion and debugging.

JavaScript example

console.log(double(3)); // 6
function double(value) { return value * 2; }
const triple = function (value) { return value * 3; };
console.log(triple(3)); // 9

console.log(double(3)); // 6
function double(value) { return value * 2; }
const triple = function (value) { return value * 3; };
console.log(triple(3)); // 9

console.log(double(3)); // 6
function double(value) { return value * 2; }
const triple = function (value) { return value * 3; };
console.log(triple(3)); // 9

Calling triple before its const initialization would throw a ReferenceError. Replacing const with var would make the earlier value undefined, so attempting to call it would instead fail as a non-callable value. I choose the form for scope, readability, and use as a value rather than performance folklore.

14. When is an arrow function different from an ordinary function?

An arrow function captures this from its surrounding lexical context instead of creating its own call-dependent this binding. It also has no own arguments object and cannot be called as a constructor with new. These properties make arrows convenient for callbacks that should retain an enclosing receiver, but usually unsuitable for object methods that need a dynamic receiver.

JavaScript example

const counter = {
  value: 4,
  makeReader() {
    return () => this.value;
  }
};
const read = counter.makeReader();
console.log(read()); // 4

const counter = {
  value: 4,
  makeReader() {
    return () => this.value;
  }
};
const read = counter.makeReader();
console.log(read()); // 4

const counter = {
  value: 4,
  makeReader() {
    return () => this.value;
  }
};
const read = counter.makeReader();
console.log(read()); // 4

An object literal does not create a new lexical this scope, so simply placing an arrow in an object does not bind it to that object. For concise object returns, parentheses matter: value => ({value}) returns an object, while braces alone introduce a function body.

15. How is this determined for a regular function call?

For a regular function, this usually follows how the function is invoked. object.method() uses the object as the receiver. A detached call does not preserve that receiver automatically. In strict mode a plain function call receives undefined as this; non-strict functions can substitute the global object. call(), apply(), and bind() let code specify a receiver, subject to the function’s own semantics.

JavaScript example

"use strict";
const account = {balance: 8, read() { return this.balance; }};
console.log(account.read()); // 8
const detached = account.read;
console.log(detached.call({balance: 5})); // 5
const bound = detached.bind(account);
console.log(bound()); // 8

"use strict";
const account = {balance: 8, read() { return this.balance; }};
console.log(account.read()); // 8
const detached = account.read;
console.log(detached.call({balance: 5})); // 5
const bound = detached.bind(account);
console.log(bound()); // 8

"use strict";
const account = {balance: 8, read() { return this.balance; }};
console.log(account.read()); // 8
const detached = account.read;
console.log(detached.call({balance: 5})); // 5
const bound = detached.bind(account);
console.log(bound()); // 8

Calling detached() directly here would throw when reading a property of undefined. Arrow functions are different because they capture this lexically. I state strictness and invocation form rather than predicting this from where a regular function was originally stored.

16. What is a closure, and how can it preserve private state?

A closure is a function together with access to the lexical environment in which it was created. That environment can outlive the outer function’s execution. Closures are useful for encapsulation, configuration, and callbacks; they retain bindings, not frozen snapshots of all values.

JavaScript example

function createCounter() {
  let count = 0;
  return {
    increment() { count += 1; return count; },
    read() { return count; }
  };
}
const a = createCounter();
const b = createCounter();
console.log(a.increment(), a.read(), b.read()); // 1 1 0

function createCounter() {
  let count = 0;
  return {
    increment() { count += 1; return count; },
    read() { return count; }
  };
}
const a = createCounter();
const b = createCounter();
console.log(a.increment(), a.read(), b.read()); // 1 1 0

function createCounter() {
  let count = 0;
  return {
    increment() { count += 1; return count; },
    read() { return count; }
  };
}
const a = createCounter();
const b = createCounter();
console.log(a.increment(), a.read(), b.read()); // 1 1 0

Each call creates an independent count binding, while the two methods returned by one call share that binding. The state is not exposed as an ordinary object property. Encapsulation does not guarantee correctness: careless closures can still retain large objects unnecessarily or read mutable state at an unexpected later time.

17. Are JavaScript arguments passed by value or by reference?

Arguments are passed by value. When the value is an object reference, the function receives a copy of that reference, so caller and callee can refer to the same object. Mutating the shared object is visible to the caller, but reassigning the parameter does not reassign the caller’s variable. This is sometimes described as call-by-sharing, not passing the caller’s variable itself by reference.

JavaScript example

function update(record) {
  record.count += 1;
  record = {count: 100};
}
const original = {count: 1};
update(original);
console.log(original.count); // 2

function update(record) {
  record.count += 1;
  record = {count: 100};
}
const original = {count: 1};
update(original);
console.log(original.count); // 2

function update(record) {
  record.count += 1;
  record = {count: 100};
}
const original = {count: 1};
update(original);
console.log(original.count); // 2

I document whether a function mutates its inputs. Returning a replacement object can make changes easier to reason about, but a shallow replacement can still share nested values. Primitive arguments do not offer an object to mutate, so changing a numeric parameter remains local.

18. How do default parameters work with undefined and null?

A default parameter is used when the argument is omitted or is explicitly undefined. It is not used for null, false, zero, or the empty string. Default expressions are evaluated when the function is called and the default is needed, rather than once when the function is defined. This makes defaults useful for creating a fresh per-call array or object.

JavaScript example

function append(value, items = []) {
  items.push(value);
  return items;
}
console.log(append(1)); // [1]
console.log(append(2)); // [2], a different array
function greet(name = "guest") { return name; }
console.log(greet(undefined), greet(null)); // "guest" null

function append(value, items = []) {
  items.push(value);
  return items;
}
console.log(append(1)); // [1]
console.log(append(2)); // [2], a different array
function greet(name = "guest") { return name; }
console.log(greet(undefined), greet(null)); // "guest" null

function append(value, items = []) {
  items.push(value);
  return items;
}
console.log(append(1)); // [1]
console.log(append(2)); // [2], a different array
function greet(name = "guest") { return name; }
console.log(greet(undefined), greet(null)); // "guest" null

The function above mutates an explicitly supplied items array, so that behavior must be intentional. Defaulting is not validation: null or a non-array argument would still need handling if callers are allowed to provide it.

19. How do rest parameters differ from spread syntax?

Rest collects remaining values into a new array, while spread expands values into another syntactic position. A rest parameter must be the last parameter. Spread in an argument list or array literal consumes an iterable; object spread instead copies enumerable own properties. The shared three-dot spelling does not make these operations identical.

JavaScript example

function sum(...values) {
  return values.reduce((total, value) => total + value, 0);
}
const values = [2, 3];
console.log(sum(...values, 4)); // 9
const copy = [...values];
console.log(copy === values); // false

function sum(...values) {
  return values.reduce((total, value) => total + value, 0);
}
const values = [2, 3];
console.log(sum(...values, 4)); // 9
const copy = [...values];
console.log(copy === values); // false

function sum(...values) {
  return values.reduce((total, value) => total + value, 0);
}
const values = [2, 3];
console.log(sum(...values, 4)); // 9
const copy = [...values];
console.log(copy === values); // false

This sum assumes numeric arguments and does not validate them. Spreading a huge collection into a function call can exceed an engine’s argument limit, so an iterative reduction is safer for unbounded data. A spread copy is shallow: nested objects still have shared identities.

20. How does destructuring make data access clearer?

Destructuring extracts selected values into bindings using an array or object pattern. Object patterns can rename fields and provide defaults; array patterns follow iterable order. Defaults apply to undefined, including a missing property, but do not replace null. I use destructuring to make a function’s inputs explicit without repeatedly writing a long property path.

JavaScript example

const input = {name: "Ana", retries: 0};
const {name: displayName, retries = 3, region = "global"} = input;
console.log(displayName, retries, region); // "Ana" 0 "global"
const [first, ...remaining] = [10, 20, 30];
console.log(first, remaining); // 10 [20, 30]

const input = {name: "Ana", retries: 0};
const {name: displayName, retries = 3, region = "global"} = input;
console.log(displayName, retries, region); // "Ana" 0 "global"
const [first, ...remaining] = [10, 20, 30];
console.log(first, remaining); // 10 [20, 30]

const input = {name: "Ana", retries: 0};
const {name: displayName, retries = 3, region = "global"} = input;
console.log(displayName, retries, region); // "Ana" 0 "global"
const [first, ...remaining] = [10, 20, 30];
console.log(first, remaining); // 10 [20, 30]

Destructuring null or undefined as an object throws, so an optional input needs an explicit fallback when appropriate. Extracting a property containing an object does not clone it, and extracting a method does not preserve its receiver for a later detached call.

21. When should you use for...of instead of for...in?

for...of reads values from an iterable, such as an array, string, Map, or Set. for...in enumerates enumerable string property keys, including inherited keys. For ordinary array values I normally choose for...of, and for a record’s own entries I choose Object.entries() with for...of. They answer different questions rather than being interchangeable loop spellings.

JavaScript example

const scores = [7, 9];
for (const score of scores) console.log(score); // 7, then 9
for (const [name, score] of Object.entries({A: 7, B: 9})) {
  console.log(name, score); // "A" 7, then "B" 9
}

const scores = [7, 9];
for (const score of scores) console.log(score); // 7, then 9
for (const [name, score] of Object.entries({A: 7, B: 9})) {
  console.log(name, score); // "A" 7, then "B" 9
}

const scores = [7, 9];
for (const score of scores) console.log(score); // 7, then 9
for (const [name, score] of Object.entries({A: 7, B: 9})) {
  console.log(name, score); // "A" 7, then "B" 9
}

An ordinary object is not iterable merely because it has properties. Unlike forEach callbacks, a loop body can use break directly. Sparse arrays and custom iterators also have defined behavior that may differ from indexed array methods, so I do not assume all iteration forms visit exactly the same positions.

22. How do map() and forEach() differ?

map() transforms an array into a new array using each callback’s return value. forEach() runs a callback for its side effects and returns undefined. I use map when the result array matters and forEach only when a side effect is the point; using map and discarding its result creates unnecessary allocation and obscures intent.

JavaScript example

const values = [1, 2, 3];
const doubled = values.map(value => value * 2);
let total = 0;
const result = values.forEach(value => { total += value; });
console.log(doubled, total, result); // [2, 4, 6] 6 undefined

const values = [1, 2, 3];
const doubled = values.map(value => value * 2);
let total = 0;
const result = values.forEach(value => { total += value; });
console.log(doubled, total, result); // [2, 4, 6] 6 undefined

const values = [1, 2, 3];
const doubled = values.map(value => value * 2);
let total = 0;
const result = values.forEach(value => { total += value; });
console.log(doubled, total, result); // [2, 4, 6] 6 undefined

Neither method automatically deep-copies elements. Their callbacks can still mutate referenced objects. Both skip missing positions in sparse arrays, and map preserves those holes. Neither waits for an async callback; asynchronous sequencing or aggregation needs a deliberate promise-aware approach.

23. How do filter(), find(), some(), and every() differ?

filter() returns all elements that satisfy a predicate. find() returns the first matching value, or undefined. some() answers whether at least one value matches, and every() answers whether all tested values match. Find, some, and every can stop early, so I choose the operation that matches the required result rather than building a full filtered array unnecessarily.

JavaScript example

const values = [2, 5, 8];
console.log(values.filter(x => x > 4)); // [5, 8]
console.log(values.find(x => x > 4));   // 5
console.log(values.some(x => x > 7));   // true
console.log(values.every(x => x > 0));  // true

const values = [2, 5, 8];
console.log(values.filter(x => x > 4)); // [5, 8]
console.log(values.find(x => x > 4));   // 5
console.log(values.some(x => x > 7));   // true
console.log(values.every(x => x > 0));  // true

const values = [2, 5, 8];
console.log(values.filter(x => x > 4)); // [5, 8]
console.log(values.find(x => x > 4));   // 5
console.log(values.some(x => x > 7));   // true
console.log(values.every(x => x > 0));  // true

For an empty array, some returns false and every returns true; universal checks must account for that. Find’s undefined result is ambiguous if undefined itself can be a valid matching element, in which case an index-based result may be clearer.

24. How do you use reduce() safely, including for an empty array?

reduce() combines array elements into one result by passing an accumulator through a callback. I normally supply an explicit initial value so the result’s type is clear and an empty array has a defined answer. Without that value, reduce uses the first present element as the initial accumulator and throws if no element exists.

JavaScript example

function sum(values) {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum([2, 4, 6]), sum([])); // 12 0

function sum(values) {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum([2, 4, 6]), sum([])); // 12 0

function sum(values) {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum([2, 4, 6]), sum([])); // 12 0

The callback must return the next accumulator; forgetting that return can turn subsequent calculations into undefined or NaN. A loop can be clearer when the operation has several branches or early termination. Repeatedly spreading a growing accumulator can introduce quadratic copying costs, so concise syntax is not automatically efficient. The example assumes an array of numbers rather than performing coercion or input validation.

25. Which common array operations mutate their input?

Operations such as push, pop, shift, unshift, splice, reverse, and sort mutate the original array. Methods such as map, filter, slice, and concat produce another array instead. ECMAScript 2023 also added copying alternatives including toSorted, toReversed, toSpliced, and with, which fit this article’s ECMAScript 2024 baseline.

JavaScript example

const original = [3, 1, 2];
const sorted = original.toSorted((a, b) => a - b);
console.log(original, sorted); // [3, 1, 2] [1, 2, 3]
original.push(4);
console.log(original); // [3, 1, 2, 4]

const original = [3, 1, 2];
const sorted = original.toSorted((a, b) => a - b);
console.log(original, sorted); // [3, 1, 2] [1, 2, 3]
original.push(4);
console.log(original); // [3, 1, 2, 4]

const original = [3, 1, 2];
const sorted = original.toSorted((a, b) => a - b);
console.log(original, sorted); // [3, 1, 2] [1, 2, 3]
original.push(4);
console.log(original); // [3, 1, 2, 4]

I choose mutation when ownership is clear and it simplifies or speeds the work, and copying when callers require the original container to remain unchanged. A newly returned array does not imply deep immutability: its object elements can still be shared. Compatibility must be checked before using newer copying methods in older runtimes.

26. How should numeric arrays be sorted?

For numeric ascending order, I pass a comparator such as (a, b) => a - b; the default sort compares string representations, not numerical magnitude. A comparator must give a consistent ordering and return a negative, zero, or positive number according to the desired relationship. Boolean-returning comparators do not correctly express both ordering directions.

JavaScript example

const values = [2, 10, 1];
console.log(values.toSorted()); // [1, 10, 2]
console.log(values.toSorted((a, b) => a - b)); // [1, 2, 10]

const values = [2, 10, 1];
console.log(values.toSorted()); // [1, 10, 2]
console.log(values.toSorted((a, b) => a - b)); // [1, 2, 10]

const values = [2, 10, 1];
console.log(values.toSorted()); // [1, 10, 2]
console.log(values.toSorted((a, b) => a - b)); // [1, 2, 10]

sort() mutates its receiver, while toSorted() preserves the input container. Modern ECMAScript requires stable sorting, so equal comparisons retain their relative order. I define explicit handling for NaN, missing fields, or mixed types instead of assuming a simple subtraction comparator supplies a meaningful domain ordering. BigInt arrays need a comparator returning Number signs, not a BigInt subtraction result.

27. How do slice() and splice() differ?

slice(start, end) returns a shallow copy of a selected range, excluding end, without removing elements from the input. splice(start, deleteCount, ...items) mutates the array by deleting or inserting elements and returns the removed elements. The second arguments have different meanings, which is a frequent source of off-by-one mistakes.

JavaScript example

const values = ["a", "b", "c", "d"];
console.log(values.slice(1, 3)); // ["b", "c"]
const removed = values.splice(1, 2, "x");
console.log(removed, values); // ["b", "c"] ["a", "x", "d"]

const values = ["a", "b", "c", "d"];
console.log(values.slice(1, 3)); // ["b", "c"]
const removed = values.splice(1, 2, "x");
console.log(removed, values); // ["b", "c"] ["a", "x", "d"]

const values = ["a", "b", "c", "d"];
console.log(values.slice(1, 3)); // ["b", "c"]
const removed = values.splice(1, 2, "x");
console.log(removed, values); // ["b", "c"] ["a", "x", "d"]

Both support negative starting positions relative to the end, subject to their normalization rules. I use toSpliced() when I want the replacement array without mutating the original; unlike splice, it returns that new array rather than the removed elements. Any copied object elements remain shared references.

28. How do you distinguish an own property from an inherited one?

Object.hasOwn(object, key) checks whether the object directly owns a property, even if its value is undefined. The in operator also considers the prototype chain. Calling object.hasOwnProperty(key) is less robust because that method may be absent or shadowed by a data field. I choose the operation according to whether inherited behavior should count.

JavaScript example

const base = {role: "reader"};
const user = Object.create(base);
user.name = undefined;
console.log("role" in user, Object.hasOwn(user, "role")); // true false
console.log(Object.hasOwn(user, "name")); // true

const base = {role: "reader"};
const user = Object.create(base);
user.name = undefined;
console.log("role" in user, Object.hasOwn(user, "role")); // true false
console.log(Object.hasOwn(user, "name")); // true

const base = {role: "reader"};
const user = Object.create(base);
user.name = undefined;
console.log("role" in user, Object.hasOwn(user, "role")); // true false
console.log(Object.hasOwn(user, "name")); // true

A value check alone cannot distinguish an absent property from a present property containing undefined. Own-property checks are especially useful when processing record-like input, but they are not a complete security validation: an allowed property name can still hold an invalid or dangerous value.

29. What does a shallow object or array copy actually copy?

A shallow copy creates a new outer container while reusing the values held by the original. When a value is itself an object, the copy and original still refer to that same nested object. Array spread, array slice, and ordinary object spread do not recursively duplicate an entire object graph.

JavaScript example

const original = {profile: {name: "A"}, enabled: true};
const copy = {...original};
copy.enabled = false;
copy.profile.name = "B";
console.log(original.enabled, original.profile.name); // true "B"
console.log(copy === original); // false

const original = {profile: {name: "A"}, enabled: true};
const copy = {...original};
copy.enabled = false;
copy.profile.name = "B";
console.log(original.enabled, original.profile.name); // true "B"
console.log(copy === original); // false

const original = {profile: {name: "A"}, enabled: true};
const copy = {...original};
copy.enabled = false;
copy.profile.name = "B";
console.log(original.enabled, original.profile.name); // true "B"
console.log(copy === original); // false

Object spread copies enumerable own properties and reads their values; it does not preserve the original prototype or property descriptors. For a nested update I copy each changed path deliberately. A structured clone is a different operation with its own supported types and limits, not a universal replacement for deciding object ownership.

30. How do optional chaining and nullish coalescing work together?

Optional chaining stops a property-access or call chain when the value being checked is null or undefined, returning undefined instead of throwing at that point. Nullish coalescing then supplies a fallback only for null or undefined. Together they preserve valid false, zero, or empty-string values that an || fallback would replace.

JavaScript example

const account = {settings: {retries: 0}};
console.log(account.settings?.retries ?? 3); // 0
console.log(account.profile?.name ?? "Guest"); // "Guest"

const account = {settings: {retries: 0}};
console.log(account.settings?.retries ?? 3); // 0
console.log(account.profile?.name ?? "Guest"); // "Guest"

const account = {settings: {retries: 0}};
console.log(account.settings?.retries ?? 3); // 0
console.log(account.profile?.name ?? "Guest"); // "Guest"

I still validate required data rather than concealing every missing field with a fallback. Optional chaining does not suppress arbitrary exceptions from getters or called functions, and optional-call syntax does not make a non-callable present value callable. It also cannot rescue an undeclared root identifier. Parenthesizing a partial chain can end its short-circuiting before a later property access.

31. How do template literals differ from quoted strings?

Template literals use backticks, support embedded expressions through ${...}, and can contain line breaks directly. They are useful for readable messages without repeated concatenation. Interpolation performs conversion to text; it does not automatically escape that text for HTML, SQL, shell commands, or any other target language.

JavaScript example

const name = "Ari";
const count = 3;
const message = `${name} has ${count} pending tasks.`;
console.log(message); // "Ari has 3 pending tasks."

const name = "Ari";
const count = 3;
const message = `${name} has ${count} pending tasks.`;
console.log(message); // "Ari has 3 pending tasks."

const name = "Ari";
const count = 3;
const message = `${name} has ${count} pending tasks.`;
console.log(message); // "Ari has 3 pending tasks."

I keep formatting separate from safe insertion into another system: a browser text node is different from constructing HTML markup. Tagged template literals are a separate facility in which a tag function receives literal segments and interpolation values. A carefully designed tag can apply a policy, but ordinary backticks alone provide no sanitization or injection protection.

32. Why can string length differ from the number of visible characters?

JavaScript strings are sequences of UTF-16 code units. The length property counts those units, not user-perceived characters. Some Unicode code points require a surrogate pair, and a visible grapheme can contain multiple code points, such as a letter plus a combining accent. String iteration with for...of or spread operates by code point, which is an improvement over code-unit indexing but is still not full grapheme segmentation.

JavaScript example

const face = "😀";
console.log(face.length, [...face].length); // 2 1
const accented = "é";
console.log(accented.length, [...accented].length); // 2 2

const face = "😀";
console.log(face.length, [...face].length); // 2 1
const accented = "é";
console.log(accented.length, [...accented].length); // 2 2

const face = "😀";
console.log(face.length, [...face].length); // 2 1
const accented = "é";
console.log(accented.length, [...accented].length); // 2 2

For user-facing cursor movement or truncation I use suitable grapheme segmentation, such as Intl.Segmenter where supported, rather than slicing arbitrary code units. Unicode normalization may help comparisons, but normalization, locale-sensitive sorting, and security-sensitive identifier rules solve different problems.

33. What are the limits of Number for decimals and large integers?

Number uses IEEE 754 binary64 floating-point representation. Many decimal fractions have no exact binary representation, and integer precision is guaranteed only within the safe-integer range from negative to positive Number.MAX_SAFE_INTEGER. I avoid using a direct equality check on computed decimal quantities when the application requires tolerance, and I define that tolerance based on the problem’s scale.

JavaScript example

console.log(0.1 + 0.2 === 0.3); // false
console.log(Number.isSafeInteger(9007199254740991)); // true
console.log(Number.isSafeInteger(9007199254740992)); // false

console.log(0.1 + 0.2 === 0.3); // false
console.log(Number.isSafeInteger(9007199254740991)); // true
console.log(Number.isSafeInteger(9007199254740992)); // false

console.log(0.1 + 0.2 === 0.3); // false
console.log(Number.isSafeInteger(9007199254740991)); // true
console.log(Number.isSafeInteger(9007199254740992)); // false

Number.EPSILON describes spacing near one; it is not a universal tolerance for all magnitudes. For financial calculations I select an explicit decimal or integer-minor-unit strategy with rounding rules and range checks. Formatting a number with toFixed produces a string and does not repair errors already introduced by earlier arithmetic.

34. When should you use BigInt, and what are its constraints?

BigInt represents integers beyond Number’s safe-integer range without losing integer precision. It uses an n suffix for literals or the BigInt conversion function. It does not represent fractions, and division truncates toward zero. Most arithmetic operations do not allow mixing BigInt and Number operands, so conversions should be explicit and checked for loss.

JavaScript example

const id = 9007199254740993n;
console.log(id + 2n); // 9007199254740995n
console.log(7n / 2n); // 3n
console.log(typeof id); // "bigint"

const id = 9007199254740993n;
console.log(id + 2n); // 9007199254740995n
console.log(7n / 2n); // 3n
console.log(typeof id); // "bigint"

const id = 9007199254740993n;
console.log(id + 2n); // 9007199254740995n
console.log(7n / 2n); // 3n
console.log(typeof id); // "bigint"

I use strings or BigInt for large identifiers instead of first parsing them as Number, which may already discard information. Default JSON serialization does not support BigInt, so an API needs an agreed string encoding or another explicit representation. BigInt also has computational costs and does not automatically make arithmetic appropriate for cryptographic use.

35. What can JSON represent, and why is it not a universal cloning tool?

JSON represents objects with string keys, arrays, strings, numbers, booleans, and null. It does not preserve arbitrary JavaScript values or object behavior. Functions, undefined, symbol values, prototypes, and property descriptors do not round-trip as general JavaScript state. Circular references throw during ordinary stringification, as does a BigInt without customized handling.

JavaScript example

const text = JSON.stringify({value: undefined, items: [undefined], n: NaN});
console.log(text); // {"items":[null],"n":null}
const data = JSON.parse('{"count":3}');
console.log(data.count); // 3

const text = JSON.stringify({value: undefined, items: [undefined], n: NaN});
console.log(text); // {"items":[null],"n":null}
const data = JSON.parse('{"count":3}');
console.log(data.count); // 3

const text = JSON.stringify({value: undefined, items: [undefined], n: NaN});
console.log(text); // {"items":[null],"n":null}
const data = JSON.parse('{"count":3}');
console.log(data.count); // 3

A Date commonly serializes through its toJSON behavior into a string rather than returning as a Date automatically. I validate parsed data against an expected schema: valid JSON syntax says nothing about trusted fields or business rules. JSON cloning is acceptable only when its loss of unsupported information is an intentional part of the contract.

36. How should try, catch, finally, and throw be used?

try encloses work that may throw, catch handles a thrown value, and finally runs as control leaves the construct, including through return or throw. I throw Error instances or subclasses so callers receive a meaningful message and stack, then catch errors where I can recover, translate them, or add useful context. Silently swallowing an unknown failure makes diagnosis harder.

JavaScript example

function parseCount(text) {
  const value = Number(text);
  if (!Number.isInteger(value) || value < 0) {
    throw new RangeError("Count must be a nonnegative integer");
  }
  return value;
}
try { parseCount("oops"); }
catch (error) { console.log(error.name); } // RangeError

function parseCount(text) {
  const value = Number(text);
  if (!Number.isInteger(value) || value < 0) {
    throw new RangeError("Count must be a nonnegative integer");
  }
  return value;
}
try { parseCount("oops"); }
catch (error) { console.log(error.name); } // RangeError

function parseCount(text) {
  const value = Number(text);
  if (!Number.isInteger(value) || value < 0) {
    throw new RangeError("Count must be a nonnegative integer");
  }
  return value;
}
try { parseCount("oops"); }
catch (error) { console.log(error.name); } // RangeError

This example allows Number’s conversion rules, including blank text becoming zero; stricter text input needs a separate check. I avoid returning from finally because it can override a pending return or thrown error. A synchronous try block does not catch a later asynchronous callback’s exception.

37. What changes when JavaScript runs in strict mode?

Strict mode opts scripts or ordinary function bodies into stricter language semantics, while ES modules and class bodies are already strict. Among other changes, assigning to an undeclared name throws rather than accidentally creating a global, and certain otherwise-silent assignment failures become errors. Plain calls to strict regular functions do not substitute the global object for an undefined receiver.

JavaScript example

"use strict";
function receiver() { return this; }
console.log(receiver()); // undefined
const fixed = Object.freeze({count: 1});
try { fixed.count = 2; }
catch (error) { console.log(error.name); } // TypeError

"use strict";
function receiver() { return this; }
console.log(receiver()); // undefined
const fixed = Object.freeze({count: 1});
try { fixed.count = 2; }
catch (error) { console.log(error.name); } // TypeError

"use strict";
function receiver() { return this; }
console.log(receiver()); // undefined
const fixed = Object.freeze({count: 1});
try { fixed.count = 2; }
catch (error) { console.log(error.name); } // TypeError

Strict mode is not a type system, sandbox, or guarantee of secure code. I prefer modules for new code and avoid assumptions based on browser classic-script globals when discussing Node modules. A use-strict directive also has syntactic restrictions in functions with non-simple parameter lists.

38. How do named and default exports work in ES modules?

Named exports expose explicitly named bindings, while a default export provides one distinguished export that an importer may name locally. A module can have multiple named exports but at most one default export. Static imports belong at module top level, and their bindings are live views of exported bindings rather than independent mutable copies.

JavaScript example

// math.mjs
export function add(a, b) { return a + b; }
export default function double(value) { return value * 2; }

// app.mjs (a separate file)
import twice, {add} from "./math.mjs";
console.log(twice(add(2, 3))); // 10

// math.mjs
export function add(a, b) { return a + b; }
export default function double(value) { return value * 2; }

// app.mjs (a separate file)
import twice, {add} from "./math.mjs";
console.log(twice(add(2, 3))); // 10

// math.mjs
export function add(a, b) { return a + b; }
export default function double(value) { return value * 2; }

// app.mjs (a separate file)
import twice, {add} from "./math.mjs";
console.log(twice(add(2, 3))); // 10

These are two separate ES-module files, not one script pasted together. Node recognizes the .mjs extension as ESM; browser module loading uses its own URL and script configuration. Module syntax does not itself define package discovery, bundling, or every host’s resolution rules. An importer cannot directly reassign an imported binding.

39. What are callbacks and higher-order functions?

A callback is a function supplied to other code for that code to invoke. A higher-order function accepts functions, returns a function, or both. Callbacks are not inherently asynchronous: array map invokes its callback synchronously, while a timer schedules a callback through its host runtime. I establish invocation timing, arguments, error behavior, and return-value use as part of a callback API’s contract.

JavaScript example

function transform(value, operation) {
  return operation(value);
}
function multiplyBy(factor) {
  return value => value * factor;
}
console.log(transform(4, multiplyBy(3))); // 12

function transform(value, operation) {
  return operation(value);
}
function multiplyBy(factor) {
  return value => value * factor;
}
console.log(transform(4, multiplyBy(3))); // 12

function transform(value, operation) {
  return operation(value);
}
function multiplyBy(factor) {
  return value => value * factor;
}
console.log(transform(4, multiplyBy(3))); // 12

Here transform accepts a function, and multiplyBy returns a closure, so both are higher-order functions. Passing operation is different from calling operation() immediately. The abstraction is useful when it removes duplication without hiding an important dependency or an unexpected side effect.

40. What makes a function pure, and why does that help testing?

A pure function returns the same result for the same inputs and has no externally observable side effects. It does not mutate caller-owned state or silently depend on changeable external state such as the current time. That makes tests easier because the result can be checked directly without resetting globals, mocking network activity, or managing shared fixtures.

JavaScript example

function applyDiscount(price, rate) {
  return price * (1 - rate);
}
console.log(applyDiscount(100, 0.2)); // 80

function applyDiscount(price, rate) {
  return price * (1 - rate);
}
console.log(applyDiscount(100, 0.2)); // 80

function applyDiscount(price, rate) {
  return price * (1 - rate);
}
console.log(applyDiscount(100, 0.2)); // 80

Purity does not imply complete input validation, precise financial rounding, or efficient execution; this simple example deliberately omits those domain policies. Real applications still need side effects. I keep calculations in small pure units and put I/O at explicit boundaries, passing required data into the calculations. Returning a new outer object while mutating a shared nested input would still violate the no-side-effects requirement.

JavaScript Interview Questions and Answers for Intermediate

These answers explain observable outcomes, error propagation, and the boundary between language and host behavior.

41. How does prototype-chain property lookup work?

Property lookup starts on the object and, if the property is absent, follows its internal prototype chain until it finds the property or reaches null. An own property shadows an inherited one; assigning a normal writable property usually creates or updates an own property rather than modifying the prototype. Inherited setters and non-writable properties can change that assignment behavior.

JavaScript example

const defaults = { retries: 3 };
const job = Object.create(defaults);
console.log(job.retries, Object.hasOwn(job, 'retries')); // 3 false
job.retries = 1;
console.log(job.retries, defaults.retries); // 1 3
delete job.retries;
console.log(job.retries); // 3

const defaults = { retries: 3 };
const job = Object.create(defaults);
console.log(job.retries, Object.hasOwn(job, 'retries')); // 3 false
job.retries = 1;
console.log(job.retries, defaults.retries); // 1 3
delete job.retries;
console.log(job.retries); // 3

const defaults = { retries: 3 };
const job = Object.create(defaults);
console.log(job.retries, Object.hasOwn(job, 'retries')); // 3 false
job.retries = 1;
console.log(job.retries, defaults.retries); // 1 3
delete job.retries;
console.log(job.retries); // 3

I use Object.getPrototypeOf to inspect inheritance and Object.hasOwn when inherited keys are not acceptable. A function's public prototype property is not the same thing as that function object's own internal prototype. Changing prototype chains dynamically also makes behavior harder to reason about.

42. How do classes relate to prototypes in JavaScript?

Classes provide structured syntax for constructing objects and sharing behavior through prototypes; they do not replace JavaScript's prototype model. Instance methods normally live on the class prototype, while public instance fields are created separately on each instance. Class bodies execute in strict mode, class constructors require new, and methods declared in the class body are non-enumerable.

JavaScript example

class Counter {
  value = 0;
  increment() { return ++this.value; }
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment); // true
console.log(a.increment(), b.value); // 1 0
console.log(Object.hasOwn(a, 'value')); // true

class Counter {
  value = 0;
  increment() { return ++this.value; }
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment); // true
console.log(a.increment(), b.value); // 1 0
console.log(Object.hasOwn(a, 'value')); // true

class Counter {
  value = 0;
  increment() { return ++this.value; }
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment); // true
console.log(a.increment(), b.value); // 1 0
console.log(Object.hasOwn(a, 'value')); // true

Calling classes merely syntactic sugar can hide meaningful semantics such as private fields and derived-constructor rules. I choose a class when identity, state, and a stable behavioral interface belong together, not simply to wrap unrelated utility functions.

43. How do extends, super, and derived constructors work?

extends establishes inheritance for instances and for the constructor objects. A derived constructor normally calls super(...) before reading this; the parent constructor initializes the instance that the subclass then extends. super.method() looks up the inherited method but invokes it with the current receiver, so the inherited method operates on the subclass instance.

JavaScript example

class Task {
  constructor(name) { this.name = name; }
  describe() { return this.name; }
}
class UrgentTask extends Task {
  constructor(name) { super(name); this.priority = 1; }
  describe() { return `${super.describe()}: urgent`; }
}
console.log(new UrgentTask('backup').describe()); // backup: urgent

class Task {
  constructor(name) { this.name = name; }
  describe() { return this.name; }
}
class UrgentTask extends Task {
  constructor(name) { super(name); this.priority = 1; }
  describe() { return `${super.describe()}: urgent`; }
}
console.log(new UrgentTask('backup').describe()); // backup: urgent

class Task {
  constructor(name) { this.name = name; }
  describe() { return this.name; }
}
class UrgentTask extends Task {
  constructor(name) { super(name); this.priority = 1; }
  describe() { return `${super.describe()}: urgent`; }
}
console.log(new UrgentTask('backup').describe()); // backup: urgent

super is tied to the method's home object, not dynamically chosen from a variable holding the receiver. I keep inheritance shallow and use composition when the relationship is sharing a capability rather than representing a substitutable subtype.

44. What do private class fields protect?

Private fields use language-enforced access checks: code outside the declaring class cannot access a field such as #balance through ordinary property syntax or reflection. They are not ordinary string-keyed properties, and a same-named private field in another class is a different private name. A subclass cannot directly access the parent's private field.

JavaScript example

class Account {
  #balance = 0;
  deposit(amount) {
    if (!Number.isFinite(amount) || amount <= 0) throw new RangeError('amount');
    this.#balance += amount;
  }
  get balance() { return this.#balance; }
}
const account = new Account();
account.deposit(5);
console.log(account.balance, Object.keys(account)); // 5 []

class Account {
  #balance = 0;
  deposit(amount) {
    if (!Number.isFinite(amount) || amount <= 0) throw new RangeError('amount');
    this.#balance += amount;
  }
  get balance() { return this.#balance; }
}
const account = new Account();
account.deposit(5);
console.log(account.balance, Object.keys(account)); // 5 []

class Account {
  #balance = 0;
  deposit(amount) {
    if (!Number.isFinite(amount) || amount <= 0) throw new RangeError('amount');
    this.#balance += amount;
  }
  get balance() { return this.#balance; }
}
const account = new Account();
account.deposit(5);
console.log(account.balance, Object.keys(account)); // 5 []

A detached method still needs a valid branded receiver; merely copying it onto another object does not grant access. Private fields protect an object's interface and invariants, not secrets from someone who controls the runtime or can observe other application outputs.

45. How do static methods differ from instance methods?

Static methods belong to the constructor, whereas instance methods belong to its prototype and are called through instances. Static methods suit factories or operations about the type that do not need a particular instance. Static methods can be inherited by subclasses; their this is the constructor used as the receiver, not an instance.

JavaScript example

class Point {
  constructor(x, y) { this.x = x; this.y = y; }
  static origin() { return new this(0, 0); }
  length() { return Math.hypot(this.x, this.y); }
}
const p = Point.origin();
console.log(p.length()); // 0
console.log(typeof p.origin); // undefined

class Point {
  constructor(x, y) { this.x = x; this.y = y; }
  static origin() { return new this(0, 0); }
  length() { return Math.hypot(this.x, this.y); }
}
const p = Point.origin();
console.log(p.length()); // 0
console.log(typeof p.origin); // undefined

class Point {
  constructor(x, y) { this.x = x; this.y = y; }
  static origin() { return new this(0, 0); }
  length() { return Math.hypot(this.x, this.y); }
}
const p = Point.origin();
console.log(p.length()); // 0
console.log(typeof p.origin); // undefined

Using new this makes this factory polymorphic, but subclasses must have a compatible construction contract. Static state is shared through constructor inheritance unless deliberately shadowed; it is not automatically copied into every instance. Private static fields have additional receiver-brand constraints.

46. How do call, apply, and bind control this?

call invokes a function with an explicit receiver and separate arguments; apply supplies the arguments from an array-like value. bind creates a new function with a fixed receiver and optionally leading arguments. They control ordinary function calls, not the lexical this captured by an arrow function.

JavaScript example

function total(tax) { return this.subtotal + tax; }
const invoice = { subtotal: 20 };
console.log(total.call(invoice, 2)); // 22
console.log(total.apply(invoice, [3])); // 23
const invoiceTotal = total.bind(invoice, 4);
console.log(invoiceTotal()); // 24

function total(tax) { return this.subtotal + tax; }
const invoice = { subtotal: 20 };
console.log(total.call(invoice, 2)); // 22
console.log(total.apply(invoice, [3])); // 23
const invoiceTotal = total.bind(invoice, 4);
console.log(invoiceTotal()); // 24

function total(tax) { return this.subtotal + tax; }
const invoice = { subtotal: 20 };
console.log(total.call(invoice, 2)); // 22
console.log(total.apply(invoice, [3])); // 23
const invoiceTotal = total.bind(invoice, 4);
console.log(invoiceTotal()); // 24

Binding is useful when passing a method as a callback because extracting a method does not preserve its original receiver. I keep the bound function reference if it must later be removed as a listener. When a constructible bound function is called with new, construction ignores the bound receiver; binding is not a universal override of every this rule.

47. Why does an arrow callback preserve its surrounding this?

An arrow function has no own this binding, so it reads this from its surrounding lexical environment. That is useful inside an instance method when a callback must keep using the same instance. The place the arrow is created matters; writing an arrow directly as an object-literal property does not make the object its receiver.

JavaScript example

class Formatter {
  constructor(prefix) { this.prefix = prefix; }
  formatAll(values) {
    return values.map(value => `${this.prefix}${value}`);
  }
}
console.log(new Formatter('#').formatAll([1, 2])); // ['#1', '#2']

class Formatter {
  constructor(prefix) { this.prefix = prefix; }
  formatAll(values) {
    return values.map(value => `${this.prefix}${value}`);
  }
}
console.log(new Formatter('#').formatAll([1, 2])); // ['#1', '#2']

class Formatter {
  constructor(prefix) { this.prefix = prefix; }
  formatAll(values) {
    return values.map(value => `${this.prefix}${value}`);
  }
}
console.log(new Formatter('#').formatAll([1, 2])); // ['#1', '#2']

Here the ordinary method receives this from its call and the arrow captures that binding. Calling formatAll after detaching it still loses the required receiver; the arrow does not repair the outer call. Arrows also lack their own arguments and cannot be used as constructors, so they are not a drop-in replacement for every function.

48. What are property descriptors and their default attributes?

A property descriptor defines either a data property, with a value and writable, or an accessor property, with get and set. Both can have enumerable and configurable. When creating a property with Object.defineProperty, omitted Boolean attributes default to false, unlike a normal object-literal data property, whose attributes are normally all true.

JavaScript example

'use strict';
const settings = {};
Object.defineProperty(settings, 'version', { value: 1, enumerable: true });
console.log(Object.keys(settings)); // ['version']
try { settings.version = 2; }
catch (error) { console.log(error.name); } // TypeError

'use strict';
const settings = {};
Object.defineProperty(settings, 'version', { value: 1, enumerable: true });
console.log(Object.keys(settings)); // ['version']
try { settings.version = 2; }
catch (error) { console.log(error.name); } // TypeError

'use strict';
const settings = {};
Object.defineProperty(settings, 'version', { value: 1, enumerable: true });
console.log(Object.keys(settings)); // ['version']
try { settings.version = 2; }
catch (error) { console.log(error.name); } // TypeError

configurable: false restricts deletion and descriptor changes, but it is not deep immutability. A non-writable property can still hold a mutable object. I inspect descriptors when cloning API objects or debugging read-only behavior rather than assuming that spreading an object preserves its property attributes.

49. When should getters and setters be used?

Getters and setters expose property syntax while running controlled behavior. A getter can derive a value; a setter can validate an update and preserve an invariant. I keep them predictable and reasonably cheap because a property read can otherwise hide expensive work or unexpected side effects. They are accessor properties, not storage slots unless backed by other state.

JavaScript example

const range = {
  _size: 1,
  get size() { return this._size; },
  set size(value) {
    if (!Number.isInteger(value) || value < 0) throw new RangeError('size');
    this._size = value;
  }
};
range.size = 3;
console.log(range.size); // 3

const range = {
  _size: 1,
  get size() { return this._size; },
  set size(value) {
    if (!Number.isInteger(value) || value < 0) throw new RangeError('size');
    this._size = value;
  }
};
range.size = 3;
console.log(range.size); // 3

const range = {
  _size: 1,
  get size() { return this._size; },
  set size(value) {
    if (!Number.isInteger(value) || value < 0) throw new RangeError('size');
    this._size = value;
  }
};
range.size = 3;
console.log(range.size); // 3

The underscore here is only a naming convention; it provides no privacy. Accessors may be inherited, and the receiver supplies this. Object spread reads enumerable getters and copies their resulting values rather than preserving the getter itself; descriptor-based copying is needed when preserving accessors is intentional.

50. Which properties do Object.keys and Reflect.ownKeys return?

Object.keys returns own enumerable string keys. Reflect.ownKeys returns all own string and symbol keys, including non-enumerable ones. Neither walks the prototype chain. For ordinary objects, own-key order places array-index keys in ascending numeric order, then other string keys in creation order, then symbol keys in creation order.

JavaScript example

const marker = Symbol('marker');
const item = { b: 1, 10: 'ten', 2: 'two', [marker]: true };
Object.defineProperty(item, 'hidden', { value: 9 });
console.log(Object.keys(item)); // ['2', '10', 'b']
console.log(Reflect.ownKeys(item).map(String));
// ['2', '10', 'b', 'hidden', 'Symbol(marker)']

const marker = Symbol('marker');
const item = { b: 1, 10: 'ten', 2: 'two', [marker]: true };
Object.defineProperty(item, 'hidden', { value: 9 });
console.log(Object.keys(item)); // ['2', '10', 'b']
console.log(Reflect.ownKeys(item).map(String));
// ['2', '10', 'b', 'hidden', 'Symbol(marker)']

const marker = Symbol('marker');
const item = { b: 1, 10: 'ten', 2: 'two', [marker]: true };
Object.defineProperty(item, 'hidden', { value: 9 });
console.log(Object.keys(item)); // ['2', '10', 'b']
console.log(Reflect.ownKeys(item).map(String));
// ['2', '10', 'b', 'hidden', 'Symbol(marker)']

Not every numeric-looking string is an array index, and exotic objects or proxies have additional rules. I choose the enumeration API according to ownership, enumerability, and symbol requirements. for...in is different because it also visits enumerable inherited string properties.

51. What are symbols useful for, and are they private?

Symbols provide unique primitive values that can be used as collision-resistant property keys or protocol identifiers. Two calls to Symbol('id') create different symbols despite having the same description. Symbol.for('id') instead uses a shared registry, and well-known symbols such as Symbol.iterator participate in language protocols.

JavaScript example

const internal = Symbol('id');
const record = { id: 'public', [internal]: 17 };
console.log(Object.keys(record)); // ['id']
console.log(record[internal]); // 17
console.log(Reflect.ownKeys(record).length); // 2

const internal = Symbol('id');
const record = { id: 'public', [internal]: 17 };
console.log(Object.keys(record)); // ['id']
console.log(record[internal]); // 17
console.log(Reflect.ownKeys(record).length); // 2

const internal = Symbol('id');
const record = { id: 'public', [internal]: 17 };
console.log(Object.keys(record)); // ['id']
console.log(record[internal]); // 17
console.log(Reflect.ownKeys(record).length); // 2

Symbols are not private: reflection can discover symbol-keyed properties, and anyone holding the symbol can use it. Enumerable symbol properties are also copied by object spread. I use a symbol to avoid accidental key collisions, not to store secrets or enforce authorization. Private class fields or closure-held state are better tools when access must be restricted by the language.

52. When should you use Map instead of an object?

I use Map for a dynamic collection of key-value associations, especially when keys are objects or when frequent insertion, deletion, and iteration are central. A map accepts keys of any type, preserves insertion order, exposes size, and separates stored entries from object properties and inherited names. Plain objects remain a natural choice for fixed-shape records.

JavaScript example

const user = { id: 1 };
const sessions = new Map([[user, 'active']]);
console.log(sessions.get(user)); // active
console.log(sessions.get({ id: 1 })); // undefined
sessions.set(user, 'idle');
console.log(sessions.size); // 1

const user = { id: 1 };
const sessions = new Map([[user, 'active']]);
console.log(sessions.get(user)); // active
console.log(sessions.get({ id: 1 })); // undefined
sessions.set(user, 'idle');
console.log(sessions.size); // 1

const user = { id: 1 };
const sessions = new Map([[user, 'active']]);
console.log(sessions.get(user)); // active
console.log(sessions.get({ id: 1 })); // undefined
sessions.set(user, 'idle');
console.log(sessions.size); // 1

Object keys use identity, not structural equality. Map key equality treats NaN as equal to itself and treats positive and negative zero as the same key. A map does not serialize its entries automatically with JSON.stringify; a deliberate interchange representation is needed. Setting a property with map.name = value does not create a map entry.

53. How does Set determine whether values are duplicates?

A Set stores unique values using SameValueZero equality: NaN matches NaN, and positive and negative zero match. Objects are compared by identity rather than by their fields. Iteration follows insertion order, making a set useful for de-duplicating primitives while retaining the first occurrence order.

JavaScript example

console.log([...new Set([3, 1, 3, NaN, NaN])]); // [3, 1, NaN]
const a = { id: 1 };
const b = { id: 1 };
console.log(new Set([a, a, b]).size); // 2

console.log([...new Set([3, 1, 3, NaN, NaN])]); // [3, 1, NaN]
const a = { id: 1 };
const b = { id: 1 };
console.log(new Set([a, a, b]).size); // 2

console.log([...new Set([3, 1, 3, NaN, NaN])]); // [3, 1, NaN]
const a = { id: 1 };
const b = { id: 1 };
console.log(new Set([a, a, b]).size); // 2

For records that are duplicates by business identity, I explicitly select a key, such as an immutable user ID, and use a map or set of those keys. Serializing whole objects to invent equality is fragile because property order, unsupported values, and normalization rules affect the result. Sets are mutable; adding one to a frozen wrapper does not freeze its stored values or entries.

54. What does WeakMap offer that Map does not?

A WeakMap associates values with weakly held keys without keeping those keys alive solely because they occur in the map. It is useful for attaching metadata to objects whose lifetime is controlled elsewhere. In the ECMAScript 2024 baseline, keys may be objects or non-registered symbols; registered symbols from Symbol.for are not valid weak keys.

JavaScript example

const metadata = new WeakMap();
const nodeLike = {};
metadata.set(nodeLike, { visited: true });
console.log(metadata.get(nodeLike).visited); // true

const metadata = new WeakMap();
const nodeLike = {};
metadata.set(nodeLike, { visited: true });
console.log(metadata.get(nodeLike).visited); // true

const metadata = new WeakMap();
const nodeLike = {};
metadata.set(nodeLike, { visited: true });
console.log(metadata.get(nodeLike).visited); // true

Weak maps have no key enumeration or size, because exposing those would make results depend on garbage-collection timing. They are not a general cache that I can list, evict by age, or measure directly. Values remain accessible while their keys are accessible. I never write correctness logic that assumes a key will be collected promptly; collection timing is intentionally nondeterministic.

55. How do shallow copies differ from structured clones?

A shallow copy creates a new outer object but retains references to nested objects. Spread, Object.assign, and array slice are common shallow operations. structuredClone, a host API available in modern browsers and Node, copies supported object graphs, preserving cycles and relationships inside the cloned graph rather than sharing ordinary nested objects with the original.

JavaScript example

const original = { config: { enabled: true } };
const shallow = { ...original };
const deep = structuredClone(original);
shallow.config.enabled = false;
console.log(original.config.enabled, deep.config.enabled); // false true

const original = { config: { enabled: true } };
const shallow = { ...original };
const deep = structuredClone(original);
shallow.config.enabled = false;
console.log(original.config.enabled, deep.config.enabled); // false true

const original = { config: { enabled: true } };
const shallow = { ...original };
const deep = structuredClone(original);
shallow.config.enabled = false;
console.log(original.config.enabled, deep.config.enabled); // false true

Structured cloning is not a universal clone: functions cannot be cloned, and custom prototypes, property descriptors, and accessor definitions are not generally preserved. Transferable buffers can be moved rather than copied when explicitly requested, detaching the original buffer. JSON round-tripping has different limitations, including loss of unsupported values and failure on cycles; I choose a representation according to the required semantics.

56. How does the iterable protocol differ from the iterator protocol?

An iterable supplies a method at Symbol.iterator that returns an iterator. An iterator supplies next(), returning objects with value and done. An object may implement either protocol or both. for...of, array spread, and many collection constructors consume iterables, not arbitrary objects that merely happen to have a next method.

JavaScript example

const countdown = {
  *[Symbol.iterator]() { yield 2; yield 1; yield 0; }
};
console.log([...countdown]); // [2, 1, 0]
console.log([...countdown]); // [2, 1, 0]

const countdown = {
  *[Symbol.iterator]() { yield 2; yield 1; yield 0; }
};
console.log([...countdown]); // [2, 1, 0]
console.log([...countdown]); // [2, 1, 0]

const countdown = {
  *[Symbol.iterator]() { yield 2; yield 1; yield 0; }
};
console.log([...countdown]); // [2, 1, 0]
console.log([...countdown]); // [2, 1, 0]

This iterable creates a fresh generator iterator each time, so separate iterations are independent. A generator object is itself iterable but normally represents one advancing sequence; spreading it again after exhaustion produces no values. I make that repeatable-versus-single-use distinction explicit in APIs, especially when callers might inspect or consume a sequence more than once.

57. How do generators pause and resume execution?

Calling a generator function creates a generator object without immediately executing its body. Each next() advances execution until a yield, a return, or an exception. A yielded value is reported with done: false; the function's final return is reported with done: true and is not included by ordinary iteration.

JavaScript example

function* conversation() {
  const name = yield 'name?';
  yield `Hello, ${name}`;
  return 'finished';
}
const iterator = conversation();
console.log(iterator.next()); // { value: 'name?', done: false }
console.log(iterator.next('Ada')); // { value: 'Hello, Ada', done: false }
console.log(iterator.next()); // { value: 'finished', done: true }

function* conversation() {
  const name = yield 'name?';
  yield `Hello, ${name}`;
  return 'finished';
}
const iterator = conversation();
console.log(iterator.next()); // { value: 'name?', done: false }
console.log(iterator.next('Ada')); // { value: 'Hello, Ada', done: false }
console.log(iterator.next()); // { value: 'finished', done: true }

function* conversation() {
  const name = yield 'name?';
  yield `Hello, ${name}`;
  return 'finished';
}
const iterator = conversation();
console.log(iterator.next()); // { value: 'name?', done: false }
console.log(iterator.next('Ada')); // { value: 'Hello, Ada', done: false }
console.log(iterator.next()); // { value: 'finished', done: true }

The value passed to the first next has no suspended yield expression to receive it. Generators support lazy sequence production and state machines, but they do not automatically run on another thread. An infinite generator must be consumed with a bound or other stopping condition.

58. How can iterator cleanup run when a loop exits early?

When for...of exits early through break, return, or many abrupt completions, it performs iterator closing by calling the iterator's return method if one exists. For a suspended generator, that normally runs finally blocks, making cleanup possible. Ordinary exhaustion also runs cleanup as execution leaves the generator body.

JavaScript example

const events = [];
function* values() {
  try { yield 1; yield 2; }
  finally { events.push('closed'); }
}
for (const value of values()) {
  events.push(value);
  break;
}
console.log(events); // [1, 'closed']

const events = [];
function* values() {
  try { yield 1; yield 2; }
  finally { events.push('closed'); }
}
for (const value of values()) {
  events.push(value);
  break;
}
console.log(events); // [1, 'closed']

const events = [];
function* values() {
  try { yield 1; yield 2; }
  finally { events.push('closed'); }
}
for (const value of values()) {
  events.push(value);
  break;
}
console.log(events); // [1, 'closed']

Dropping a reference to a suspended iterator is not the same as closing it and does not guarantee deterministic cleanup. I avoid yielding again from cleanup code because that can suspend the generator before cleanup is finished. Resources with important lifetimes should have explicit ownership and an explicit close path rather than relying on garbage collection.

59. How do per-iteration let bindings differ from a shared var binding in closures?

A for loop with a lexical let declaration creates a fresh binding for each iteration, so callbacks created in different iterations can retain different values. A var declaration instead uses one binding in its enclosing var scope, such as the containing function in this snippet; all callbacks read that same binding when they later execute. Closures capture bindings, not automatically copied snapshots.

JavaScript example

function makeCallbacks() {
  const lexical = [];
  const shared = [];
  for (let i = 0; i < 3; i++) lexical.push(() => i);
  for (var j = 0; j < 3; j++) shared.push(() => j);
  return [lexical.map(fn => fn()), shared.map(fn => fn())];
}
console.log(makeCallbacks()); // [[0, 1, 2], [3, 3, 3]]

function makeCallbacks() {
  const lexical = [];
  const shared = [];
  for (let i = 0; i < 3; i++) lexical.push(() => i);
  for (var j = 0; j < 3; j++) shared.push(() => j);
  return [lexical.map(fn => fn()), shared.map(fn => fn())];
}
console.log(makeCallbacks()); // [[0, 1, 2], [3, 3, 3]]

function makeCallbacks() {
  const lexical = [];
  const shared = [];
  for (let i = 0; i < 3; i++) lexical.push(() => i);
  for (var j = 0; j < 3; j++) shared.push(() => j);
  return [lexical.map(fn => fn()), shared.map(fn => fn())];
}
console.log(makeCallbacks()); // [[0, 1, 2], [3, 3, 3]]

No timers or event-loop assumptions are involved: both loops finish before any callback is invoked. A separate function invocation can also create a distinct captured binding when working with older var-based code. I prefer lexical declarations when each iteration represents independent callback state.

60. What are ES module live bindings?

An ES module import refers to the exporting module's binding rather than copying its value once. If the exporter changes an exported let, importers observe the new value. The importer cannot reassign that imported binding, though an exported object may still be mutable through its properties.

JavaScript example

// counter.mjs
export let count = 0;
export function increment() { count += 1; }

// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1

// counter.mjs
export let count = 0;
export function increment() { count += 1; }

// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1

// counter.mjs
export let count = 0;
export function increment() { count += 1; }

// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1

Run these as two files, not as one concatenated script. Modules also have their own scope and run in strict mode. Static import declarations participate in linking before ordinary module execution; they are not equivalent to destructuring a function's return value. Module resolution and loading rules belong partly to the host, so browser URL resolution and Node package resolution need not behave identically.

61. Why can circular module dependencies fail during initialization?

Circular ES module imports are allowed, but an import can be accessed before its exporting lexical declaration has initialized. Reading that binding then throws a ReferenceError. The problem is evaluation order and early access, not simply the existence of a cycle. A cycle may work when imported values are only read later, after initialization has completed.

For example, if module A imports B, and B computes a top-level constant from A's exported const before A reaches that declaration, B observes the temporal dead zone. Function declarations and deferred function calls can change the result, so I inspect the actual declarations and execution path rather than predicting all cycles will fail.

I prefer breaking problematic cycles by extracting shared contracts, injecting dependencies, or moving initialization to an explicit composition step. Top-level await adds asynchronous dependencies and can further complicate initialization. A successful development build is not evidence that every cycle is robust across refactors or loading paths.

62. When should you use dynamic import?

Dynamic import(specifier) loads a module on demand and returns a promise for its module namespace object. It is useful for optional features, expensive dependencies, or environment-specific paths. Unlike static imports, it can appear inside a conditional or function, and failures must be handled asynchronously.

JavaScript example

async function formatWithPlugin(value) {
  const { format } = await import('./formatter.mjs');
  return format(value);
}
// formatter.mjs must export a format function.

async function formatWithPlugin(value) {
  const { format } = await import('./formatter.mjs');
  return format(value);
}
// formatter.mjs must export a format function.

async function formatWithPlugin(value) {
  const { format } = await import('./formatter.mjs');
  return format(value);
}
// formatter.mjs must export a format function.

This is a two-module integration example; it is not executable without the plugin module. The language defines the operation, while the host and bundler determine resolution, fetching, and whether a separate download is produced. Repeated imports of the same resolved module normally reuse its module instance rather than re-running initialization. I keep specifiers constrained rather than treating arbitrary user input as a trusted module location, and distinguish load failures from failures inside the loaded feature.

63. What does resolving a promise actually mean?

Resolving a promise commits it to a result, which may be another promise or thenable; it does not necessarily mean the promise has fulfilled yet. A promise can be resolved while still pending if it is adopting a pending promise. Its settled state is either fulfilled or rejected, and later attempts to change the outcome have no effect.

JavaScript example

let release;
const inner = new Promise(resolve => { release = resolve; });
const outer = new Promise(resolve => { resolve(inner); });
outer.then(value => console.log(value));
release(7); // Eventually logs 7 through the promise reaction.

let release;
const inner = new Promise(resolve => { release = resolve; });
const outer = new Promise(resolve => { resolve(inner); });
outer.then(value => console.log(value));
release(7); // Eventually logs 7 through the promise reaction.

let release;
const inner = new Promise(resolve => { release = resolve; });
const outer = new Promise(resolve => { resolve(inner); });
outer.then(value => console.log(value));
release(7); // Eventually logs 7 through the promise reaction.

The executor passed to new Promise runs synchronously, but registered reactions run asynchronously through promise jobs. Promise resolution assimilates callable then properties, so returning arbitrary thenables can execute external behavior. I use Promise.resolve to adopt values and promise-like results rather than confusing adoption with immediate successful completion.

64. How do returned values and thrown errors affect promise chains?

Each then call returns a new promise. Returning a normal value fulfills that new promise with the value; returning a promise makes it adopt that promise's outcome; throwing rejects it. A missing handler propagates the previous outcome. A catch that returns a normal value recovers the chain, so silently returning from an error handler can unintentionally turn failure into success.

JavaScript example

Promise.resolve(2)
  .then(value => value * 3)
  .then(() => { throw new Error('stop'); })
  .catch(error => error.message)
  .then(value => console.log(value)); // stop

Promise.resolve(2)
  .then(value => value * 3)
  .then(() => { throw new Error('stop'); })
  .catch(error => error.message)
  .then(value => console.log(value)); // stop

Promise.resolve(2)
  .then(value => value * 3)
  .then(() => { throw new Error('stop'); })
  .catch(error => error.message)
  .then(value => console.log(value)); // stop

I return asynchronous work from handlers so the chain can track it. Starting work without returning or awaiting it creates a detached operation whose errors and completion are not represented by the parent chain. finally is for cleanup and normally preserves the prior outcome, but throwing or returning a rejected promise from it replaces that outcome with a rejection.

65. How does Promise.all differ from sequential awaiting?

Promise.all combines independent operations and fulfills with their results in input order, regardless of completion order. It rejects when an input rejects, but it does not cancel other operations. Sequential await starts the next operation only after the previous awaited operation completes when the calls themselves are placed in sequence.

JavaScript example

async function loadPair(loadLeft, loadRight) {
  const [left, right] = await Promise.all([loadLeft(), loadRight()]);
  return { left, right };
}
loadPair(() => Promise.resolve('L'), () => Promise.resolve('R'))
  .then(console.log); // { left: 'L', right: 'R' }

async function loadPair(loadLeft, loadRight) {
  const [left, right] = await Promise.all([loadLeft(), loadRight()]);
  return { left, right };
}
loadPair(() => Promise.resolve('L'), () => Promise.resolve('R'))
  .then(console.log); // { left: 'L', right: 'R' }

async function loadPair(loadLeft, loadRight) {
  const [left, right] = await Promise.all([loadLeft(), loadRight()]);
  return { left, right };
}
loadPair(() => Promise.resolve('L'), () => Promise.resolve('R'))
  .then(console.log); // { left: 'L', right: 'R' }

The calls start work here; Promise.all is not a scheduler or thread launcher. I use concurrency only when operations do not depend on each other and the downstream service can handle the load. Large inputs need a concurrency limit. When operations are dependent, sequential awaiting is correct rather than a performance mistake.

66. When should you choose Promise.allSettled, any, or race?

I choose the combinator according to the result policy. Promise.allSettled waits for every input and returns a status record for each, so partial success remains visible. Promise.any fulfills with the first fulfillment and rejects with an AggregateError only if every input rejects. Promise.race adopts the first settlement, whether fulfillment or rejection.

JavaScript example

Promise.allSettled([Promise.resolve(1), Promise.reject('offline')])
  .then(results => console.log(results.map(result => result.status)));
// ['fulfilled', 'rejected']

Promise.allSettled([Promise.resolve(1), Promise.reject('offline')])
  .then(results => console.log(results.map(result => result.status)));
// ['fulfilled', 'rejected']

Promise.allSettled([Promise.resolve(1), Promise.reject('offline')])
  .then(results => console.log(results.map(result => result.status)));
// ['fulfilled', 'rejected']

None of these combinators cancels losing or unfinished work. A timeout implemented by racing a timer against a request therefore needs separate cancellation and timer cleanup. Empty inputs are also different: allSettled([]) fulfills with an empty array, any([]) rejects with an AggregateError, and race([]) remains pending. I test these boundary cases when accepting a dynamically constructed operation list.

67. How does async/await change control flow and error handling?

An async function always returns a promise. await suspends that function's continuation until the awaited value is adopted; fulfillment becomes a value and rejection becomes a throw at the await expression. Other work on the same event loop can proceed while the function is suspended. Awaiting an already fulfilled promise still resumes asynchronously.

JavaScript example

async function readAmount(load) {
  try {
    const value = await load();
    if (!Number.isFinite(value)) throw new TypeError('amount');
    return value;
  } catch (error) {
    throw new Error('Cannot read amount', { cause: error });
  }
}

async function readAmount(load) {
  try {
    const value = await load();
    if (!Number.isFinite(value)) throw new TypeError('amount');
    return value;
  } catch (error) {
    throw new Error('Cannot read amount', { cause: error });
  }
}

async function readAmount(load) {
  try {
    const value = await load();
    if (!Number.isFinite(value)) throw new TypeError('amount');
    return value;
  } catch (error) {
    throw new Error('Cannot read amount', { cause: error });
  }
}

This function also catches a synchronous throw from calling load. In contrast, returning load() directly from the try would not catch a later rejection there; return await load() would. I preserve useful error causes and place recovery at the layer that can make a real decision rather than swallowing every error.

68. Why is async forEach usually wrong for awaited work?

forEach ignores the return value of its callback, so it does not await promises returned by an async callback. The outer operation can finish before the work completes, and callback rejections can go unhandled. I use for...of with await for ordered work or map to promises and await a combinator for independent work.

JavaScript example

async function sequential(values, save) {
  for (const value of values) await save(value);
}
async function concurrent(values, save) {
  await Promise.all(values.map(value => save(value)));
}

async function sequential(values, save) {
  for (const value of values) await save(value);
}
async function concurrent(values, save) {
  await Promise.all(values.map(value => save(value)));
}

async function sequential(values, save) {
  for (const value of values) await save(value);
}
async function concurrent(values, save) {
  await Promise.all(values.map(value => save(value)));
}

These functions resolve only after the work they track succeeds, or reject when a tracked failure occurs. The concurrent version may leave other saves running after one rejects; it does not provide transactional rollback. For a large list, I use a bounded worker pool rather than launching everything at once. I also decide explicitly whether one failure stops processing or is recorded alongside successful results.

69. How do tasks and microtasks interact in a browser event loop?

In a browser, the HTML event loop schedules tasks such as timer callbacks and user events. Promise reactions and queueMicrotask callbacks use the microtask queue. At a microtask checkpoint, queued microtasks are processed until the queue is empty, including microtasks added during that checkpoint. Rendering opportunities are integrated into the event loop; a repaint is not guaranteed after every task.

JavaScript example

console.log('sync');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
// In this isolated browser script: sync, promise, microtask, timer.

console.log('sync');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
// In this isolated browser script: sync, promise, microtask, timer.

console.log('sync');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
// In this isolated browser script: sync, promise, microtask, timer.

This ordering assumes ordinary execution of this snippet, not debugger intervention or surrounding asynchronous activity. Recursively enqueueing microtasks can delay tasks and rendering. ECMAScript defines promise jobs, but DOM events, timers, and rendering are host behavior. Node has a different event-loop integration and additional queues, so I do not generalize every browser ordering puzzle to Node.

70. Why is a zero-delay timer not immediate?

A zero-delay timer asks the host to make a callback eligible after a delay; it does not interrupt the current JavaScript execution or promise an exact execution time. The callback waits until the current work and relevant microtask processing allow a timer task to run. Browser timer nesting, background-page throttling, and a busy main thread can increase the effective delay.

I use timers for approximate scheduling, not as a substitute for an explicit readiness signal. Waiting a guessed number of milliseconds for DOM content or network data is brittle; a promise, event, or documented lifecycle signal represents the actual dependency. Repeating timers can also drift under load, so elapsed-time calculations should use a suitable clock rather than assuming each callback arrived exactly on schedule.

setTimeout is a host API, not part of the ECMAScript language specification. Node timer behavior has its own integration rules. Clearing a timer prevents a pending callback from being scheduled normally; it cannot interrupt a callback that is already executing.

71. What does AbortController cancel, and what does it not cancel?

AbortController communicates cancellation through an AbortSignal to APIs that explicitly support it. Calling abort() changes the signal state and notifies listeners; it does not cancel arbitrary promises or terminate JavaScript code. A fetch can stop client-side request or body-consumption work, but cancellation does not prove that the server never received or acted on the request.

JavaScript example

async function loadData(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
// The caller creates a controller, passes controller.signal,
// and calls controller.abort() when this result is no longer needed.

async function loadData(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
// The caller creates a controller, passes controller.signal,
// and calls controller.abort() when this result is no longer needed.

async function loadData(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
// The caller creates a controller, passes controller.signal,
// and calls controller.abort() when this result is no longer needed.

This browser/Node fetch example is illustrative and makes no request until called. A signal stays aborted, so a new operation needs a new controller when reuse is inappropriate. Custom operations should check already-aborted signals, remove listeners during cleanup, and propagate meaningful cancellation reasons rather than misreporting all failures as cancellation.

72. How should fetch handle HTTP errors and response bodies?

fetch normally fulfills when an HTTP response is available even if its status is 404 or 500. I check response.ok or the specific accepted statuses before parsing the body, and treat transport failure, HTTP failure, and invalid response data as distinct errors. Fetch is a host API; browser policies such as CORS are separate from JavaScript language semantics.

JavaScript example

async function fetchJson(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  if (response.status === 204) return null;
  return response.json();
}

async function fetchJson(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  if (response.status === 204) return null;
  return response.json();
}

async function fetchJson(url, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  if (response.status === 204) return null;
  return response.json();
}

The function is an unexecuted integration example. json() can reject on invalid JSON, and successful parsing does not validate the application's expected schema. Response bodies are streams and ordinarily consumed once. If two consumers genuinely need the body, clone the response before consumption and consider buffering costs. I handle cancellation and credentials explicitly rather than assuming fetch supplies application-level retries or authorization.

73. How do event capture, targeting, and bubbling work?

DOM event dispatch follows an event path. Capture listeners on ancestors run on the way toward the target, listeners run at the target, and bubbling listeners run outward when the event's bubbles flag permits it. event.target identifies the target exposed to that listener, while event.currentTarget identifies the object whose listener is currently executing.

JavaScript example

// Browser only; assumes outer contains button.
outer.addEventListener('click', () => console.log('capture'), { capture: true });
button.addEventListener('click', () => console.log('target'));
outer.addEventListener('click', () => console.log('bubble'));
// An ordinary click on button logs capture, target, bubble.

// Browser only; assumes outer contains button.
outer.addEventListener('click', () => console.log('capture'), { capture: true });
button.addEventListener('click', () => console.log('target'));
outer.addEventListener('click', () => console.log('bubble'));
// An ordinary click on button logs capture, target, bubble.

// Browser only; assumes outer contains button.
outer.addEventListener('click', () => console.log('capture'), { capture: true });
button.addEventListener('click', () => console.log('target'));
outer.addEventListener('click', () => console.log('bubble'));
// An ordinary click on button logs capture, target, bubble.

Not every event bubbles, and shadow DOM can retarget events or restrict crossing a boundary according to composed. Capture is not a separate event sent before the real event; it is a phase of the same dispatch. I inspect the event type and DOM structure before relying on a propagation pattern.

74. How do preventDefault and propagation controls differ?

preventDefault() asks the browser to cancel a cancelable event's default action, such as navigation or form submission. It does not stop other listeners from receiving the event. stopPropagation() stops further propagation through the event path but does not stop other listeners already applicable on the same target. stopImmediatePropagation() also prevents later listeners on that target from running.

For a form's submit handler, I use preventDefault() when application code genuinely replaces submission, and then implement the intended behavior accessibly. I do not stop propagation merely to avoid understanding a parent listener, because that can break unrelated controls, analytics, or delegated handling.

Cancellation has limits: an event must be cancelable, and a passive listener cannot cancel its default action through preventDefault. Returning false from an addEventListener callback has no special cancellation meaning. These are DOM event rules, not general rules for arbitrary JavaScript callback APIs.

75. How can event delegation handle dynamically added elements?

Event delegation attaches a listener to a stable ancestor and identifies the relevant descendant from the event. Because newly added descendants participate in propagation, they usually do not need individual listeners. I verify that the matched element belongs to the intended container and choose an event that bubbles or an appropriate capture strategy.

JavaScript example

// Browser only; list is an existing Element containing action buttons.
list.addEventListener('click', event => {
  const target = event.target;
  if (!(target instanceof Element)) return;
  const button = target.closest('button[data-action]');
  if (!button || !list.contains(button)) return;
  console.log(button.dataset.action);
});

// Browser only; list is an existing Element containing action buttons.
list.addEventListener('click', event => {
  const target = event.target;
  if (!(target instanceof Element)) return;
  const button = target.closest('button[data-action]');
  if (!button || !list.contains(button)) return;
  console.log(button.dataset.action);
});

// Browser only; list is an existing Element containing action buttons.
list.addEventListener('click', event => {
  const target = event.target;
  if (!(target instanceof Element)) return;
  const button = target.closest('button[data-action]');
  if (!button || !list.contains(button)) return;
  console.log(button.dataset.action);
});

closest lets a click on a nested icon resolve to its button. This sample assumes elements from the current realm and no shadow-boundary retargeting; cross-frame and shadow DOM designs need additional care. I validate the allowed action names before invoking privileged behavior. A data attribute identifies intent but is not a security boundary.

76. How do you remove event listeners reliably?

To remove a listener with removeEventListener, I retain the same callback object and match the event type and capture flag used at registration. Recreating an arrow function or calling bind again creates a different function and will not remove the earlier listener. I usually tie listener cleanup to the owner's lifecycle.

JavaScript example

// Browser only; button is an existing Element.
const controller = new AbortController();
const onClick = () => console.log('clicked');
button.addEventListener('click', onClick, { signal: controller.signal });
// During owner teardown:
controller.abort();

// Browser only; button is an existing Element.
const controller = new AbortController();
const onClick = () => console.log('clicked');
button.addEventListener('click', onClick, { signal: controller.signal });
// During owner teardown:
controller.abort();

// Browser only; button is an existing Element.
const controller = new AbortController();
const onClick = () => console.log('clicked');
button.addEventListener('click', onClick, { signal: controller.signal });
// During owner teardown:
controller.abort();

An abort signal can remove a group of associated listeners, while { once: true } removes a listener after its first invocation. Neither pattern excuses retaining unnecessary application objects elsewhere. When removing manually, options such as passive do not need to match, but capture does. Cleanup prevents stale behavior and can release references held by long-lived event targets.

77. How do DOM attributes differ from element properties?

Attributes are values in the element's markup representation, usually read and written as strings. DOM properties are JavaScript-facing state and can have typed values, default behavior, or relationships to attributes defined by the element's interface. Some properties reflect attributes, but not every property is a live mirror of the corresponding attribute.

For a text input, the value attribute establishes the initial/default value, while the value property represents the current value after editing. Similarly, checked is the current checked state and defaultChecked reflects the attribute. A Boolean attribute's presence means true: writing disabled="false" still disables a control.

I use properties when controlling current form state and the documented attribute interface when changing markup-oriented configuration. Custom data-* attributes are accessible through dataset, whose values are strings. I avoid assuming that changing an attribute will reproduce a user's interaction or dispatch input/change events; those are separate behaviors.

78. How do live DOM collections differ from static query results?

A live DOM collection reflects later changes to the underlying document, whereas a static collection represents the matching nodes when the query ran. getElementsByClassName returns a live HTMLCollection; querySelectorAll returns a static NodeList. The type name alone is not enough: some other NodeList instances, such as childNodes, are live.

JavaScript example

// Browser only; container is an existing Element.
const snapshot = Array.from(container.getElementsByClassName('temporary'));
for (const element of snapshot) element.remove();

// Browser only; container is an existing Element.
const snapshot = Array.from(container.getElementsByClassName('temporary'));
for (const element of snapshot) element.remove();

// Browser only; container is an existing Element.
const snapshot = Array.from(container.getElementsByClassName('temporary'));
for (const element of snapshot) element.remove();

Creating an array snapshot avoids skipping nodes when removals would shift a live collection during indexed iteration. Static membership does not freeze the nodes themselves: a referenced element can still change or become detached. I choose one query when I need a stable working set, and requery deliberately when the algorithm needs current membership. Large retained snapshots can also keep detached nodes reachable.

79. When should DOM work use requestAnimationFrame?

requestAnimationFrame schedules a browser callback for a rendering update opportunity, making it appropriate for animation and batching visual changes. It is not a fixed-rate timer or a promise that painting will happen immediately after the callback. Refresh rates vary, and callbacks are commonly paused or throttled in non-visible documents.

JavaScript example

// Browser only; box is an existing Element.
let start;
function frame(timestamp) {
  start ??= timestamp;
  const progress = Math.min((timestamp - start) / 500, 1);
  box.style.transform = `translateX(${progress * 100}px)`;
  if (progress < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

// Browser only; box is an existing Element.
let start;
function frame(timestamp) {
  start ??= timestamp;
  const progress = Math.min((timestamp - start) / 500, 1);
  box.style.transform = `translateX(${progress * 100}px)`;
  if (progress < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

// Browser only; box is an existing Element.
let start;
function frame(timestamp) {
  start ??= timestamp;
  const progress = Math.min((timestamp - start) / 500, 1);
  box.style.transform = `translateX(${progress * 100}px)`;
  if (progress < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

The supplied timestamp makes progress time-based rather than frame-count-based. Production code should retain the request ID for cancellation and respect reduced-motion preferences. I avoid mixing repeated layout reads and writes in the same loop, since forced synchronous layout can still make a frame expensive. This API belongs to the browser rendering environment, not ECMAScript itself.

80. How do async iterables differ from ordinary iterables?

An async iterable exposes Symbol.asyncIterator, whose iterator methods return promises for iteration results. for await...of awaits each result and supports asynchronous sequences such as streamed data or paginated records. An ordinary iterable supplies synchronous iteration results; for await...of can also consume it through an adaptation that awaits yielded values.

JavaScript example

async function* pages() {
  yield ['a', 'b'];
  yield ['c'];
}
async function collect() {
  const values = [];
  for await (const page of pages()) values.push(...page);
  return values;
}
collect().then(console.log); // ['a', 'b', 'c']

async function* pages() {
  yield ['a', 'b'];
  yield ['c'];
}
async function collect() {
  const values = [];
  for await (const page of pages()) values.push(...page);
  return values;
}
collect().then(console.log); // ['a', 'b', 'c']

async function* pages() {
  yield ['a', 'b'];
  yield ['c'];
}
async function collect() {
  const values = [];
  for await (const page of pages()) values.push(...page);
  return values;
}
collect().then(console.log); // ['a', 'b', 'c']

This example is local and performs no network requests. Async iteration provides a way to request the next value, but does not automatically impose a bounded buffer on every producer implementation. Early loop exit calls the iterator's return when available and awaits cleanup. I design cancellation, resource ownership, and errors explicitly when adapting real streams.

JavaScript Interview Questions and Answers for Experienced

These answers connect implementation choices to invariants, measurable evidence, and safe failure handling.

81. How would you limit concurrent asynchronous operations without changing result order?

I would start a fixed number of workers, give each worker the next input index, and store its result at that index. Starting every promise first and then awaiting them in batches does not limit the underlying work.

JavaScript example

async function mapLimited(items, limit, task) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new RangeError("limit must be a positive integer");
  }
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await task(items[index], index);
    }
  }
  await Promise.all(Array.from(
    { length: Math.min(limit, items.length) }, worker
  ));
  return results;
}
// await mapLimited([3, 1, 2], 2, async n => n * 2)
// resolves to [6, 2, 4].

async function mapLimited(items, limit, task) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new RangeError("limit must be a positive integer");
  }
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await task(items[index], index);
    }
  }
  await Promise.all(Array.from(
    { length: Math.min(limit, items.length) }, worker
  ));
  return results;
}
// await mapLimited([3, 1, 2], 2, async n => n * 2)
// resolves to [6, 2, 4].

async function mapLimited(items, limit, task) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new RangeError("limit must be a positive integer");
  }
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await task(items[index], index);
    }
  }
  await Promise.all(Array.from(
    { length: Math.min(limit, items.length) }, worker
  ));
  return results;
}
// await mapLimited([3, 1, 2], 2, async n => n * 2)
// resolves to [6, 2, 4].

This function accepts a dense array and returns an empty array for empty input. A rejection rejects the result but does not cancel other workers; they can continue processing. A fail-stop or cancellation requirement needs an explicit shared stop flag and cooperative cancellation. For an unbounded input source, I would use a streaming design rather than retain all inputs and results.

82. How do you prevent stale asynchronous results from overwriting newer state?

I would associate each request with a generation number and commit its result only if it still belongs to the current generation. Cancelling an obsolete request can save resources, but a generation check separately protects the state from APIs that ignore cancellation or have already completed.

JavaScript example

function createLatestLoader(load, commit) {
  let generation = 0;
  return async function refresh(query) {
    const mine = ++generation;
    const value = await load(query);
    if (mine === generation) commit(value);
    return value;
  };
}

function createLatestLoader(load, commit) {
  let generation = 0;
  return async function refresh(query) {
    const mine = ++generation;
    const value = await load(query);
    if (mine === generation) commit(value);
    return value;
  };
}

function createLatestLoader(load, commit) {
  let generation = 0;
  return async function refresh(query) {
    const mine = ++generation;
    const value = await load(query);
    if (mine === generation) commit(value);
    return value;
  };
}

If request A starts, then B starts, and B resolves first, only B may commit even if A resolves later. Callers still need to handle rejected refresh promises. Loading and error indicators need the same generation check; otherwise an old failure can replace a new success. I would also invalidate the generation when the owning view is disposed, and define whether repeated identical queries should share work.

83. What makes a retry policy safe rather than merely persistent?

A safe retry policy retries only failures that are plausibly transient, within an overall deadline and a bounded attempt budget. It uses exponential backoff with jitter and respects relevant server guidance. Validation failures and authorization failures normally need correction, not repetition.

The most important question is whether repeating the operation is safe. A timeout does not prove that the server failed to commit a payment or create a record. For a non-idempotent action, I would use a server-enforced idempotency key or reconcile the result before retrying. The same logical operation must retain the same key across attempts.

I would inject the clock, delay, and randomness for deterministic tests, propagate cancellation during both requests and waiting, and cap concurrency so retries do not amplify an outage. Observability should distinguish logical operations from attempts. A retry loop that eventually succeeds but overloads its dependency is not a successful resilience design.

84. Why can race conditions occur in single-threaded JavaScript?

Single-threaded execution prevents two ordinary callbacks on the same agent from running simultaneously, but it does not make a sequence spanning await atomic. Other work can run while the function is suspended, so a decision made before suspension may no longer be valid afterward.

JavaScript example

let balance = 100;
async function unsafeDebit(amount, approve) {
  if (balance >= amount) {
    await approve();
    balance -= amount;
  }
}

let balance = 100;
async function unsafeDebit(amount, approve) {
  if (balance >= amount) {
    await approve();
    balance -= amount;
  }
}

let balance = 100;
async function unsafeDebit(amount, approve) {
  if (balance >= amount) {
    await approve();
    balance -= amount;
  }
}

Two calls for 80 can both pass the initial check and eventually produce a negative balance. I would put the invariant inside an appropriate transaction, serialize the local operations, or use optimistic concurrency with a version check and retry policy. A local queue protects only that process; it does not coordinate multiple servers or browser tabs. Shared-memory workers introduce actual parallel access and need a separate synchronization design.

85. How should cancellation propagate through an asynchronous workflow?

Cancellation should be explicit, cooperative, and passed down to every operation that can support it. The caller owns the decision to cancel; a child operation should not abort an unrelated caller-owned controller. At each boundary I would specify whether cancellation rejects, returns a partial result, or completes already-committed work.

For browser or Node APIs accepting AbortSignal, I pass the signal into the actual I/O, check an already-aborted signal before starting additional work, and remove custom abort listeners when the operation settles. Cancellation does not automatically undo an external side effect or interrupt a long synchronous loop.

I also stop scheduling new work after cancellation, clean up acquired resources in finally, and distinguish cancellation from failures in telemetry. Promise rejection alone is not cancellation: rejecting a wrapper while an underlying task continues can leak capacity. Tests must cover cancellation before start, during waiting, and near successful completion.

86. How does backpressure prevent an asynchronous pipeline from exhausting memory?

Backpressure means the consumer can slow the producer instead of letting queued work grow without bound. It matters when data arrives faster than it can be transformed, persisted, or transmitted. A concurrency limit controls active operations, but an unlimited waiting queue can still exhaust memory.

For Node writable streams, a false return from write means the producer should stop writing until drain, not that the chunk was rejected. I generally prefer pipeline to manually wiring many stream lifecycle events. For an asynchronous iterator, pulling one item at a time provides a useful demand boundary, but the producer may still buffer internally.

I would specify queue limits, overflow behavior, cancellation, and error propagation across the whole chain. Metrics should include queue depth and time waiting, not just throughput. When dropping data is acceptable, the policy must be explicit—for example, retaining the latest UI update rather than silently discarding financial events.

87. How would you design an asynchronous iterator with reliable cleanup?

I would put resource cleanup in finally inside the async generator and make the acquisition and close contracts explicit. A consumer that breaks out of for await normally requests iterator closure, which lets the generator run its finally block.

JavaScript example

async function* readRows(openCursor) {
  const cursor = await openCursor();
  try {
    for (;;) {
      const row = await cursor.nextRow();
      if (row === null) return; // Contract: null means end.
      yield row;
    }
  } finally {
    await cursor.close();
  }
}

async function* readRows(openCursor) {
  const cursor = await openCursor();
  try {
    for (;;) {
      const row = await cursor.nextRow();
      if (row === null) return; // Contract: null means end.
      yield row;
    }
  } finally {
    await cursor.close();
  }
}

async function* readRows(openCursor) {
  const cursor = await openCursor();
  try {
    for (;;) {
      const row = await cursor.nextRow();
      if (row === null) return; // Contract: null means end.
      yield row;
    }
  } finally {
    await cursor.close();
  }
}

This example assumes nextRow and close are asynchronous methods and null is not a valid row. It does not make a hung nextRow cancellable; the cursor must support cancellation or a deadline for that. A manually managed iterator must also be closed when abandoned. I would decide how to report a close failure without losing the primary processing error, and test normal exhaustion, early break, and read failure.

88. What changes when reasoning about the Node.js event loop instead of a browser event loop?

Node has host-specific event-loop phases, including timers, poll, and check; a browser uses the HTML event-loop model with tasks, microtask checkpoints, and rendering opportunities. Neither model is specified by ECMAScript alone. I would identify the runtime and whether code runs as CommonJS or an ES module before predicting ordering involving Node-specific queues.

In Node, setImmediate schedules work in the check phase, while setTimeout establishes a timer threshold rather than an exact execution time. Their order from top-level code is not a portable guarantee. process.nextTick is a Node-specific queue and excessive use can starve I/O. ES-module evaluation can also change simplistic nextTick-versus-promise examples.

I rely on documented ordering only when the scheduling context is explicit. For application coordination I use promises, events, or explicit state transitions rather than competing zero-delay timers. Browser rendering behavior cannot be established by executing the same code in Node.

89. When should CPU-heavy JavaScript move to a worker?

I would move sufficiently expensive CPU work to a worker when it blocks responsiveness or reduces server throughput, and when the benefit exceeds messaging and startup overhead. Making a CPU-heavy function async does not move its computation off the current thread.

Browser Web Workers execute in separate worker contexts and cannot directly manipulate the document DOM. Node worker_threads are useful for CPU-intensive JavaScript; routine asynchronous I/O normally does not need a worker per request. I would use a bounded worker pool for recurring tasks, define message schemas, and plan cancellation and worker failure recovery.

Data ownership matters: cloning a large object graph can cost more than the computation, while transferring supported buffers changes which side owns them. I would measure end-to-end latency, serialization cost, queueing, and memory, not just the worker's compute time. Shared state requires deliberate synchronization rather than assuming message handlers make all data access safe.

90. How would you process large data with Node.js streams?

I would compose readable, transform, and writable streams with pipeline so backpressure and many error/cleanup concerns are handled consistently. The promise-based API makes completion observable without collecting the whole dataset in memory.

JavaScript example

// Node.js ESM; in-memory illustration, no file or network I/O.
import { Readable, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
const output = [];
await pipeline(
  Readable.from(["a", "b"]),
  new Writable({
    objectMode: true,
    write(chunk, encoding, callback) {
      output.push(chunk.toUpperCase());
      callback();
    }
  })
);
console.log(output); // ["A", "B"]

// Node.js ESM; in-memory illustration, no file or network I/O.
import { Readable, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
const output = [];
await pipeline(
  Readable.from(["a", "b"]),
  new Writable({
    objectMode: true,
    write(chunk, encoding, callback) {
      output.push(chunk.toUpperCase());
      callback();
    }
  })
);
console.log(output); // ["A", "B"]

// Node.js ESM; in-memory illustration, no file or network I/O.
import { Readable, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
const output = [];
await pipeline(
  Readable.from(["a", "b"]),
  new Writable({
    objectMode: true,
    write(chunk, encoding, callback) {
      output.push(chunk.toUpperCase());
      callback();
    }
  })
);
console.log(output); // ["A", "B"]

The collected output is only a tiny test fixture; a real large-data sink should not retain every chunk. I would choose byte or object mode intentionally, keep transforms incremental, and handle records that span chunk boundaries. pipeline may destroy streams on error, so stream reuse and HTTP-response behavior need particular care. Successful completion is different from merely starting the pipeline.

91. How do you investigate a JavaScript memory leak?

I would first demonstrate retained memory growth under a repeatable workload, allowing for garbage collection and legitimate caches. A rising allocation rate is not necessarily a leak; the key issue is memory that remains reachable after its useful lifetime.

I would compare heap snapshots, inspect retaining paths, and investigate long-lived maps, closures, subscriptions, timers, detached DOM nodes, and unfinished asynchronous operations. In Node I would distinguish JavaScript heap usage from buffers and other native allocations. For a browser view, repeatedly mounting and disposing the same feature is a useful reproduction.

The fix usually changes ownership or lifecycle: remove a listener, abort an operation, bound a cache, or release a reference. I would then rerun the workload and verify a stable trend rather than celebrate one smaller snapshot. Heap snapshots can expose sensitive values and pause a process, so production collection needs appropriate safeguards.

92. Why are WeakRef and FinalizationRegistry unsuitable for essential cleanup?

WeakRef allows a reference that does not itself keep its target alive, and FinalizationRegistry can request a callback after a registered target becomes unreachable. Neither gives a dependable cleanup time or guarantees that a finalizer will run before the process or page ends.

I would not use them to release locks, close files, commit transactions, or unsubscribe essential resources. Those tasks need explicit disposal and try/finally. A finalizer is, at most, a fallback or diagnostic aid; code must remain correct if it never runs.

Weak references can support specialized caches, but retrieving a value must handle an absent target and use the retrieved strong reference consistently. I also avoid finalizer held values or callback closures that retain the target, which defeats collection. Tests should verify the surrounding behavior without requiring garbage collection to occur on a particular schedule.

93. How would you build a bounded cache rather than an accidental memory leak?

I would define a cache's maximum size, expiration policy, key identity, and invalidation rules before adding it. Memoization without an eviction policy can turn every unique request into permanently retained memory.

JavaScript example

function createFifoCache(maxEntries) {
  if (!Number.isInteger(maxEntries) || maxEntries < 1) {
    throw new RangeError("positive maxEntries required");
  }
  const values = new Map();
  return {
    get: key => values.get(key),
    set(key, value) {
      if (!values.has(key) && values.size === maxEntries) {
        values.delete(values.keys().next().value);
      }
      values.set(key, value);
    },
    get size() { return values.size; }
  };
}

function createFifoCache(maxEntries) {
  if (!Number.isInteger(maxEntries) || maxEntries < 1) {
    throw new RangeError("positive maxEntries required");
  }
  const values = new Map();
  return {
    get: key => values.get(key),
    set(key, value) {
      if (!values.has(key) && values.size === maxEntries) {
        values.delete(values.keys().next().value);
      }
      values.set(key, value);
    },
    get size() { return values.size; }
  };
}

function createFifoCache(maxEntries) {
  if (!Number.isInteger(maxEntries) || maxEntries < 1) {
    throw new RangeError("positive maxEntries required");
  }
  const values = new Map();
  return {
    get: key => values.get(key),
    set(key, value) {
      if (!values.has(key) && values.size === maxEntries) {
        values.delete(values.keys().next().value);
      }
      values.set(key, value);
    },
    get size() { return values.size; }
  };
}

This is FIFO, not LRU: reading or updating an existing key does not refresh its insertion position. It limits entry count, not bytes, and get cannot distinguish a missing entry from a stored undefined. A production cache may need has, byte accounting, TTL, tenant isolation, or explicit invalidation. I would measure hit rate and memory cost to prove that the cache is beneficial.

94. How do you profile JavaScript before optimizing it?

I would begin with a user-visible performance target and a representative workload, then profile the application to locate the actual bottleneck. Browser CPU profiles, network traces, long tasks, and rendering information answer different questions; Node CPU and memory profiles expose server-side costs.

I separate CPU time, waiting for I/O, queueing, allocation, and rendering. A slow endpoint caused by a database round trip will not be fixed by changing a loop syntax. For a candidate optimization, I compare before and after using consistent inputs, sufficient repetitions, warm-up where relevant, and distributions rather than one timing.

Microbenchmarks can help isolate a hypothesis but may not reflect real engine optimization, garbage collection, or production data. I also check correctness and memory tradeoffs. The final evidence is improved end-to-end behavior without unacceptable regressions, not a claim that one JavaScript construct is universally faster.

95. How can a change in data structure improve algorithmic performance?

I would first remove unnecessary repeated work. If each of n records scans m selected identifiers with includes, the membership portion can approach n times m comparisons. Building a Set once usually makes repeated membership checks much cheaper in practice.

JavaScript example

function selectedRecords(records, selectedIds) {
  const selected = new Set(selectedIds);
  return records.filter(record => selected.has(record.id));
}
console.log(selectedRecords(
  [{ id: 1 }, { id: 2 }, { id: 3 }], [3, 1, 1]
)); // [{ id: 1 }, { id: 3 }]

function selectedRecords(records, selectedIds) {
  const selected = new Set(selectedIds);
  return records.filter(record => selected.has(record.id));
}
console.log(selectedRecords(
  [{ id: 1 }, { id: 2 }, { id: 3 }], [3, 1, 1]
)); // [{ id: 1 }, { id: 3 }]

function selectedRecords(records, selectedIds) {
  const selected = new Set(selectedIds);
  return records.filter(record => selected.has(record.id));
}
console.log(selectedRecords(
  [{ id: 1 }, { id: 2 }, { id: 3 }], [3, 1, 1]
)); // [{ id: 1 }, { id: 3 }]

The result follows the records' order, and duplicate selected identifiers do not duplicate output. Set uses SameValueZero equality, so object identifiers compare by identity rather than by structural contents. ECMAScript requires average access time to be sublinear but does not promise a particular hash-table implementation or worst-case constant time. For small inputs, setup overhead may dominate; I would measure before claiming a practical win.

96. How do debounce and throttle differ in a production interface?

Debounce waits for a quiet period before invoking an operation; throttle limits invocation frequency during a continuing stream. Debounce suits a search field that should settle before querying. Throttle suits periodic progress updates where ongoing feedback matters.

JavaScript example

function debounce(fn, delay) {
  let timer;
  function wrapped(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = undefined;
      fn.apply(this, args);
    }, delay);
  }
  wrapped.cancel = () => {
    clearTimeout(timer);
    timer = undefined;
  };
  return wrapped;
}

function debounce(fn, delay) {
  let timer;
  function wrapped(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = undefined;
      fn.apply(this, args);
    }, delay);
  }
  wrapped.cancel = () => {
    clearTimeout(timer);
    timer = undefined;
  };
  return wrapped;
}

function debounce(fn, delay) {
  let timer;
  function wrapped(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = undefined;
      fn.apply(this, args);
    }, delay);
  }
  wrapped.cancel = () => {
    clearTimeout(timer);
    timer = undefined;
  };
  return wrapped;
}

This trailing-edge implementation uses host timers, preserves the last call's receiver and arguments, and offers cancellation for disposal. It does not return the eventual result, handle asynchronous rejections, or guarantee execution at an exact time. I would specify leading versus trailing behavior, maximum wait, and what happens to pending work on teardown. Debouncing network calls alone also does not prevent old responses from overwriting newer results.

97. Why can repeated microtasks make a browser interface unresponsive?

Microtasks are drained at checkpoints before the browser proceeds to later work, including many rendering opportunities. A chain that keeps adding microtasks can therefore monopolize the event loop even though each callback is short. Awaiting an already-resolved promise yields to promise processing, not necessarily to input handling or painting.

For a large computation I would divide work into bounded chunks and yield through an appropriate host scheduling mechanism, or move the computation to a worker. A timer creates a task boundary but has clamping and background-throttling behavior; it is not a precise frame scheduler. requestAnimationFrame is appropriate for visual updates, not unlimited computation inside one frame.

I would measure responsiveness and input latency, not only total completion time. The best chunk size depends on the workload and device. Making a loop async without creating meaningful opportunities for the browser to do other work does not solve blocking.

98. How would you avoid layout thrashing in browser code?

I would group layout-dependent reads separately from DOM writes instead of repeatedly alternating them. A read such as getBoundingClientRect after a style change can force the browser to bring layout up to date, making an otherwise small loop expensive.

JavaScript example

// Browser-only: elements is a fixed array of DOM elements.
const widths = elements.map(element =>
  element.getBoundingClientRect().width
);
elements.forEach((element, index) => {
  element.style.height = `${widths[index]}px`;
});

// Browser-only: elements is a fixed array of DOM elements.
const widths = elements.map(element =>
  element.getBoundingClientRect().width
);
elements.forEach((element, index) => {
  element.style.height = `${widths[index]}px`;
});

// Browser-only: elements is a fixed array of DOM elements.
const widths = elements.map(element =>
  element.getBoundingClientRect().width
);
elements.forEach((element, index) => {
  element.style.height = `${widths[index]}px`;
});

This reads the current widths before writing heights, reducing read/write interleaving. It does not guarantee zero layout cost, and the desired measurement semantics must be clear when style changes affect later measurements. I would use CSS layout when it can express the relationship directly, and coordinate visual changes with rendering where appropriate. DevTools performance traces can confirm forced layouts. Node-based tests cannot verify actual layout, paint, or frame timing.

99. How do you test asynchronous code without relying on arbitrary delays?

I would await the promise that represents completion and control the relevant dependencies explicitly. Sleeping for an arbitrary number of milliseconds makes a test both slow and unreliable because machine load and scheduler behavior are not part of the business contract.

JavaScript example

function deferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}
const gate = deferred();
const result = gate.promise.then(value => value * 2);
gate.resolve(4);
// await result is 8.

function deferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}
const gate = deferred();
const result = gate.promise.then(value => value * 2);
gate.resolve(4);
// await result is 8.

function deferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}
const gate = deferred();
const result = gate.promise.then(value => value * 2);
gate.resolve(4);
// await result is 8.

A deferred promise lets a test choose exactly when an operation finishes and can reproduce out-of-order completion. I would test rejection and cancellation as well as success, and ensure the test runner observes every asynchronous assertion. Returning neither a promise nor a completion signal can let a test pass before its assertions run. Host-specific behavior still needs integration tests in the intended runtime.

100. What are the limits of fake timers in JavaScript tests?

Fake timers replace selected clock or scheduling APIs so a test can advance virtual time without waiting in real time. They are useful for debounce, timeouts, and retry schedules, but they do not reproduce an entire browser or operating-system scheduler.

I would check which APIs the test runner mocks: timers, Date, performance clocks, and promise microtasks may be treated differently. Advancing a timer can schedule promise continuations that still need to be awaited. Repeatedly running all timers can also loop forever when the code intentionally reschedules itself.

A fake-timer unit test proves the application's scheduling decisions under that model; it does not prove actual rendering, network timing, worker scheduling, or Node event-loop phase behavior. I would restore timers after each test, avoid mixing real and fake clocks accidentally, and retain a small number of real-runtime integration tests for the relevant boundaries.

101. How does dependency injection improve testability without requiring a framework?

Dependency injection means supplying an operation's dependencies instead of hard-coding them. A plain function parameter or factory is often enough; no container or framework is required.

JavaScript example

function makeGreeting({ readName, now }) {
  return async id => {
    const name = await readName(id);
    return { message: `Hello, ${name}`, at: now() };
  };
}
const greet = makeGreeting({
  readName: async () => "Ada",
  now: () => 123
});
// await greet("user-1") is { message: "Hello, Ada", at: 123 }.

function makeGreeting({ readName, now }) {
  return async id => {
    const name = await readName(id);
    return { message: `Hello, ${name}`, at: now() };
  };
}
const greet = makeGreeting({
  readName: async () => "Ada",
  now: () => 123
});
// await greet("user-1") is { message: "Hello, Ada", at: 123 }.

function makeGreeting({ readName, now }) {
  return async id => {
    const name = await readName(id);
    return { message: `Hello, ${name}`, at: now() };
  };
}
const greet = makeGreeting({
  readName: async () => "Ada",
  now: () => 123
});
// await greet("user-1") is { message: "Hello, Ada", at: 123 }.

The test can provide a deterministic clock and controlled I/O without mutating global state. I would inject boundaries such as storage, network, clocks, or randomness rather than every small pure helper. The dependencies still need clear contracts, and a fake must not silently behave differently from the production implementation. Integration tests verify that the real adapters honor those contracts. This design also makes error and cancellation scenarios easier to reproduce.

102. What properties would you test beyond a few example inputs?

I would test invariants and relationships that should hold for a broad input domain, not just a few hand-picked outputs. For a nonmutating numeric sort, useful properties include preserving length and element multiplicity, producing nondecreasing order, and leaving the original input unchanged.

Round-trip properties are useful when the domain is explicit: decoding an encoded value should reproduce it only for values the format actually supports. JSON round trips do not preserve arbitrary JavaScript objects, undefined, BigInt, or every numeric value.

I would generate boundary cases such as empty collections, duplicates, extreme sizes, Unicode strings, and invalid inputs. When a property fails, reducing the input to a minimal counterexample makes debugging practical. Property-based tests complement example-based tests; a flawed invariant can pass for the wrong implementation. For concurrent code I would also vary controlled completion order and assert that invariants survive every tested ordering.

103. How do you prevent DOM-based cross-site scripting?

I would keep untrusted data out of executable HTML, script, and URL contexts. For plain text in a browser DOM node, textContent is normally the correct sink rather than innerHTML.

JavaScript example

// Browser-only: status is a DOM element; message is untrusted text.
status.textContent = message;

// Browser-only: status is a DOM element; message is untrusted text.
status.textContent = message;

// Browser-only: status is a DOM element; message is untrusted text.
status.textContent = message;

If the feature genuinely accepts rich HTML, I would use a maintained sanitizer configured for the allowed content and review dangerous attributes and URL schemes. Hand-written substitutions are not a general HTML sanitizer, and escaping appropriate for text is not automatically correct for an attribute, JavaScript, CSS, or URL context.

Content Security Policy and Trusted Types can provide additional protection where supported, but they do not replace safe data handling. I would inspect data flow from sources to sinks, including third-party widgets and stored content. Server validation is valuable, yet DOM-based injection can still occur entirely within client-side code.

104. How do you prevent prototype pollution when accepting dynamic keys?

I would avoid recursively copying arbitrary attacker-controlled keys into ordinary objects. Prototype pollution can arise when dynamic assignments traverse special property paths and alter an inherited prototype, affecting unrelated objects.

JavaScript example

function readOptions(input) {
  const result = Object.create(null);
  for (const key of ["theme", "locale"]) {
    if (Object.hasOwn(input, key) && typeof input[key] === "string") {
      result[key] = input[key];
    }
  }
  return result;
}

function readOptions(input) {
  const result = Object.create(null);
  for (const key of ["theme", "locale"]) {
    if (Object.hasOwn(input, key) && typeof input[key] === "string") {
      result[key] = input[key];
    }
  }
  return result;
}

function readOptions(input) {
  const result = Object.create(null);
  for (const key of ["theme", "locale"]) {
    if (Object.hasOwn(input, key) && typeof input[key] === "string") {
      result[key] = input[key];
    }
  }
  return result;
}

This example assumes input is an already-parsed, non-null JSON object. It copies an allowlisted schema into a null-prototype result rather than merging unknown keys. For dictionary data, Map or null-prototype objects can reduce inherited-property hazards, but they are not a substitute for validating every write path. I would reject dangerous paths in generic nesting utilities, use own-property checks, and avoid treating a missing inherited authorization flag as an explicit trusted value.

105. Why should configuration data not be executed with eval or Function?

I would parse configuration as data and validate it against an explicit schema, not execute it as source code. eval and Function cross a major trust boundary: a configuration value can become arbitrary code with access to capabilities available in its execution environment.

JSON.parse is appropriate for JSON syntax, but parsing alone does not make the resulting data trustworthy. I would validate types, allowed keys, size, and domain constraints before using it. For a formula feature, I would define a deliberately limited grammar and interpreter rather than expose general JavaScript execution.

Function does not become safe simply because it cannot capture the caller's local lexical scope. It can still access global capabilities, and policy restrictions may block dynamic code generation. If executing genuinely untrusted programs is a requirement, I would choose a properly isolated execution boundary with resource limits instead of calling an object wrapper a sandbox.

106. Where should validation and authorization occur in a JavaScript system?

Validation belongs at every trust boundary, and authorization must be enforced by the system that owns the protected resource. Browser validation improves feedback but cannot secure a server API because clients can be modified or bypassed.

I would validate request shape, type, length, ranges, and supported identifiers at the server boundary, then authorize the specific action on the specific resource for the authenticated principal. An authenticated user is not automatically allowed to read another user's record. Database constraints and transactions enforce invariants that must survive concurrent requests.

Inside trusted code, validation can be lighter if contracts are explicit, but data loaded from storage, external services, or messages may need renewed validation. I would avoid coercing malformed values into valid-looking defaults. Error responses should be useful without disclosing secrets, and logging must not turn rejected credentials or private payloads into another data exposure.

107. How should a module expose a small, stable public API?

I would expose the operations and data contracts callers actually need while keeping mutable implementation details private. A stable API defines inputs, outputs, failure behavior, timing, cancellation, and ownership; exported function names alone are not the contract.

JavaScript example

function createCounter(initial = 0) {
  if (!Number.isSafeInteger(initial)) throw new TypeError("safe integer required");
  let value = initial;
  return Object.freeze({
    read: () => value,
    increment() {
      if (value === Number.MAX_SAFE_INTEGER) throw new RangeError("overflow");
      return ++value;
    }
  });
}

function createCounter(initial = 0) {
  if (!Number.isSafeInteger(initial)) throw new TypeError("safe integer required");
  let value = initial;
  return Object.freeze({
    read: () => value,
    increment() {
      if (value === Number.MAX_SAFE_INTEGER) throw new RangeError("overflow");
      return ++value;
    }
  });
}

function createCounter(initial = 0) {
  if (!Number.isSafeInteger(initial)) throw new TypeError("safe integer required");
  let value = initial;
  return Object.freeze({
    read: () => value,
    increment() {
      if (value === Number.MAX_SAFE_INTEGER) throw new RangeError("overflow");
      return ++value;
    }
  });
}

The returned API cannot be replaced through ordinary property writes, while its internal state intentionally remains mutable. I would document that distinction instead of calling the counter immutable. For larger modules I would separate pure domain logic from environment adapters, avoid surprising import-time side effects, and maintain compatibility tests for public behavior. Smaller interfaces are easier to test and change without exposing accidental implementation promises.

108. What does immutable state mean when JavaScript objects are nested?

Immutable state means an existing value is not changed when producing the next state. A new outer object alone is insufficient if code later mutates nested objects still shared with the previous state.

JavaScript example

const previous = { user: { name: "Ada" }, count: 1 };
const next = {
  ...previous,
  user: { ...previous.user, name: "Grace" }
};
console.log(previous.user.name, next.user.name); // Ada Grace
console.log(previous.user === next.user); // false

const previous = { user: { name: "Ada" }, count: 1 };
const next = {
  ...previous,
  user: { ...previous.user, name: "Grace" }
};
console.log(previous.user.name, next.user.name); // Ada Grace
console.log(previous.user === next.user); // false

const previous = { user: { name: "Ada" }, count: 1 };
const next = {
  ...previous,
  user: { ...previous.user, name: "Grace" }
};
console.log(previous.user.name, next.user.name); // Ada Grace
console.log(previous.user === next.user); // false

This copies only the path being changed, preserving other references when safe. Object.freeze is shallow: it prevents ordinary changes to that object's own properties but does not recursively freeze nested values or make every built-in's internal state immutable. Deep cloning everything can be expensive and can change semantics. I would establish ownership rules, use structural sharing carefully, and test that reducers or transformation functions do not mutate their inputs. Type-system readonly annotations, when used, are not runtime enforcement.

109. How should a JavaScript API represent recoverable failures?

I would make expected domain failures distinguishable from unexpected faults and keep that distinction consistent across the API. A discriminated result object can be useful when callers routinely branch on a recoverable condition, while thrown errors work well for exceptional failures that should propagate.

JavaScript example

function parsePositiveInteger(text) {
  if (typeof text !== "string" || !/^[1-9]\d*$/.test(text)) {
    return { ok: false, code: "INVALID_FORMAT" };
  }
  const value = Number(text);
  return Number.isSafeInteger(value)
    ? { ok: true, value }
    : { ok: false, code: "OUT_OF_RANGE" };
}

function parsePositiveInteger(text) {
  if (typeof text !== "string" || !/^[1-9]\d*$/.test(text)) {
    return { ok: false, code: "INVALID_FORMAT" };
  }
  const value = Number(text);
  return Number.isSafeInteger(value)
    ? { ok: true, value }
    : { ok: false, code: "OUT_OF_RANGE" };
}

function parsePositiveInteger(text) {
  if (typeof text !== "string" || !/^[1-9]\d*$/.test(text)) {
    return { ok: false, code: "INVALID_FORMAT" };
  }
  const value = Number(text);
  return Number.isSafeInteger(value)
    ? { ok: true, value }
    : { ok: false, code: "OUT_OF_RANGE" };
}

This deliberately rejects whitespace, zero, signs, and leading zeros; those are contract decisions, not universal parsing rules. I would not silently return undefined for every failure because it loses meaning and is easy to ignore. For asynchronous functions, I would also specify whether cancellation is a rejected promise or a tagged result. Logging and retries belong at boundaries that understand the operation, not in every helper.

110. How do you design a serialization boundary that survives schema changes?

I would serialize an explicit data-transfer schema rather than an arbitrary live object graph. The boundary should define field names, optional versus missing fields, versioning, numeric limits, dates, and how unknown fields are handled.

JSON is interoperable but does not preserve prototypes, functions, undefined object properties, or cyclic references, and JSON.stringify rejects BigInt unless custom handling is provided. Non-finite numbers become null. A date string also needs an agreed timezone and format; reconstructing a Date is an application decision.

For identifiers larger than the safe integer range, I would use strings and validate them rather than lose precision before transmission. I would add backward-compatible fields when possible, tolerate documented optional fields, and test old/new reader combinations. Deserialization is not authorization: received values still need validation. Explicit schemas make migrations and cross-language integration far more predictable than relying on JavaScript's in-memory representation.

111. When should you use typed arrays and DataView?

I use typed arrays for fixed-width numeric views over binary storage, such as image data, audio samples, or protocol buffers. DataView is better when a binary format mixes field types or requires explicit byte order and offsets.

JavaScript example

const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint16(0, 0x1234, false); // Big-endian.
view.setUint16(2, 0x5678, true);  // Little-endian.
console.log([...new Uint8Array(buffer)]); // [18, 52, 120, 86]

const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint16(0, 0x1234, false); // Big-endian.
view.setUint16(2, 0x5678, true);  // Little-endian.
console.log([...new Uint8Array(buffer)]); // [18, 52, 120, 86]

const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint16(0, 0x1234, false); // Big-endian.
view.setUint16(2, 0x5678, true);  // Little-endian.
console.log([...new Uint8Array(buffer)]); // [18, 52, 120, 86]

Several views can refer to the same buffer, so writes through one can be observed through another. Typed arrays are not ordinary growable arrays and numeric conversions follow the selected element type; for example, integer storage can wrap rather than reject an out-of-range value. I would validate protocol lengths and offsets before reading and avoid assuming the host's native byte order matches the wire format. Shared or transferred buffers also require explicit ownership and lifetime rules.

112. How do structured cloning and transferable objects differ?

Structured cloning copies supported data graphs, including many values that JSON cannot represent, such as cycles and Map. Transferring a transferable object moves its usable underlying resource to the receiving side instead of copying that resource, leaving the original unusable in the relevant way.

JavaScript example

const original = new Uint8Array([1, 2, 3]);
const copy = structuredClone(original, {
  transfer: [original.buffer]
});
console.log(copy[0]); // 1
console.log(original.buffer.byteLength); // 0: detached.

const original = new Uint8Array([1, 2, 3]);
const copy = structuredClone(original, {
  transfer: [original.buffer]
});
console.log(copy[0]); // 1
console.log(original.buffer.byteLength); // 0: detached.

const original = new Uint8Array([1, 2, 3]);
const copy = structuredClone(original, {
  transfer: [original.buffer]
});
console.log(copy[0]); // 1
console.log(original.buffer.byteLength); // 0: detached.

structuredClone is a host API available in modern browsers and Node, not an ECMAScript global specified by ECMA-262. This example transfers an ordinary ArrayBuffer. It does not mean arbitrary objects, functions, or DOM nodes are transferable or cloneable. Custom class prototypes and property descriptors are not generally preserved as an object-model cloning mechanism. I would document that the sender relinquishes ownership and avoid accessing detached views afterward. SharedArrayBuffer, when available, shares memory instead of being transferred.

113. When are SharedArrayBuffer and Atomics appropriate?

SharedArrayBuffer is appropriate when multiple agents genuinely need shared memory and the performance benefit justifies synchronization complexity. Atomics provides operations with defined shared-memory ordering for supported integer typed-array views, avoiding unsafe read-modify-write sequences.

JavaScript example

const shared = new SharedArrayBuffer(4);
const counter = new Int32Array(shared);
console.log(Atomics.add(counter, 0, 1)); // 0: previous value.
console.log(Atomics.load(counter, 0));   // 1

const shared = new SharedArrayBuffer(4);
const counter = new Int32Array(shared);
console.log(Atomics.add(counter, 0, 1)); // 0: previous value.
console.log(Atomics.load(counter, 0));   // 1

const shared = new SharedArrayBuffer(4);
const counter = new Int32Array(shared);
console.log(Atomics.add(counter, 0, 1)); // 0: previous value.
console.log(Atomics.load(counter, 0));   // 1

This single-agent illustration checks API semantics, not parallel correctness. In actual workers, counter[0]++ is not a substitute for Atomics.add because it can lose concurrent updates. Atomics does not automatically make a multi-step protocol correct; I would define its state transitions and happens-before relationships. Browser availability is constrained by security requirements such as cross-origin isolation, and blocking waits are not permitted on the browser main thread. Message passing is often simpler and safer when shared memory is not essential.

114. How should browser storage choices reflect consistency and security requirements?

I choose storage based on data volume, lifetime, access patterns, and trust rather than convenience alone. localStorage and sessionStorage provide synchronous string storage and can block the main thread. IndexedDB supports larger structured data and transactions through asynchronous APIs. Cookies have different request, lifetime, and security semantics.

Browser storage is not a trusted authority and may be unavailable, cleared, quota-limited, or evicted. I would handle failures and treat offline data as a cache or explicitly designed local source of truth. Multi-tab updates require a consistency strategy; a read followed by a write in localStorage is not a cross-tab transaction.

Sensitive authentication decisions remain server-side. JavaScript-readable storage is exposed to successful same-origin script injection; HttpOnly cookies reduce script access but introduce their own request and CSRF considerations. I would store the minimum needed, validate loaded data, and define migration and logout cleanup behavior.

115. How do you design event subscriptions so components can be disposed safely?

I would make every subscription return an explicit disposal operation, and give the owning component responsibility for calling it. Cleanup should be idempotent so multiple teardown paths do not cause secondary failures.

JavaScript example

function subscribe(target, type, listener, options) {
  target.addEventListener(type, listener, options);
  const capture = typeof options === "boolean"
    ? options : Boolean(options?.capture);
  let active = true;
  return () => {
    if (!active) return;
    active = false;
    target.removeEventListener(type, listener, capture);
  };
}

function subscribe(target, type, listener, options) {
  target.addEventListener(type, listener, options);
  const capture = typeof options === "boolean"
    ? options : Boolean(options?.capture);
  let active = true;
  return () => {
    if (!active) return;
    active = false;
    target.removeEventListener(type, listener, capture);
  };
}

function subscribe(target, type, listener, options) {
  target.addEventListener(type, listener, options);
  const capture = typeof options === "boolean"
    ? options : Boolean(options?.capture);
  let active = true;
  return () => {
    if (!active) return;
    active = false;
    target.removeEventListener(type, listener, capture);
  };
}

This browser/EventTarget helper assumes it owns this registration; independently registering the same type, listener, and capture tuple is not a separate reference-counted subscription. It records the capture value used for removal and reuses the same callback identity. I would also cancel associated timers and asynchronous work, since removing an event listener does not cancel a callback already running. For DOM listeners, a dedicated AbortController can group related registrations, but controller lifetime must match their ownership.

116. How do you avoid duplicate work with an in-flight promise cache?

I would cache the promise for an operation that is currently running, then remove it when it settles. This coalesces simultaneous requests without turning transient failures or old values into permanent cache entries.

JavaScript example

function singleFlight(load) {
  const pending = new Map();
  return function get(key) {
    if (pending.has(key)) return pending.get(key);
    const promise = Promise.resolve().then(() => load(key));
    pending.set(key, promise);
    const clear = () => {
      if (pending.get(key) === promise) pending.delete(key);
    };
    promise.then(clear, clear);
    return promise;
  };
}

function singleFlight(load) {
  const pending = new Map();
  return function get(key) {
    if (pending.has(key)) return pending.get(key);
    const promise = Promise.resolve().then(() => load(key));
    pending.set(key, promise);
    const clear = () => {
      if (pending.get(key) === promise) pending.delete(key);
    };
    promise.then(clear, clear);
    return promise;
  };
}

function singleFlight(load) {
  const pending = new Map();
  return function get(key) {
    if (pending.has(key)) return pending.get(key);
    const promise = Promise.resolve().then(() => load(key));
    pending.set(key, promise);
    const clear = () => {
      if (pending.get(key) === promise) pending.delete(key);
    };
    promise.then(clear, clear);
    return promise;
  };
}

The deferred call converts a synchronous throw from load into rejection. Both settlement handlers clean the cache, and the returned promise retains the original result or failure. Keys must include all inputs affecting the answer, including tenant or authorization context. Shared work complicates cancellation: one caller should not abort an operation still needed by others. I would keep result caching, concurrency limits, and subscription cancellation as separate policies.

117. How should timeout handling differ from cancellation?

A timeout limits how long a caller is willing to wait; cancellation requests that the underlying work stop. Racing an operation against a rejecting timer implements a waiting limit but leaves the losing operation running unless it supports cancellation.

I would pass an AbortSignal into compatible APIs, ensure timers are cleared on every completion path, and distinguish deadline expiry from a user cancellation when that distinction matters. A timeout does not prove that a remote side effect failed, so retries still require idempotency or reconciliation.

The deadline should usually cover queueing and retries as well as an individual attempt. For CPU work, a signal only helps if the code checks it or the worker can be terminated under a defined recovery policy. Tests should cover completion just before expiry, cancellation before start, and late results after the caller has already stopped waiting.

118. How do you decide between sequential, concurrent, and parallel execution?

I choose sequential execution when each step depends on the previous result or when strict ordering is part of correctness. I choose bounded concurrency for independent I/O so waiting can overlap without overwhelming the dependency. I choose parallel execution for CPU work when multiple workers or processes can make progress simultaneously.

JavaScript example

// Independent, already-safe operations may be started together.
const [profile, settings] = await Promise.all([
  loadProfile(),
  loadSettings()
]);

// Independent, already-safe operations may be started together.
const [profile, settings] = await Promise.all([
  loadProfile(),
  loadSettings()
]);

// Independent, already-safe operations may be started together.
const [profile, settings] = await Promise.all([
  loadProfile(),
  loadSettings()
]);

This assumes both loaders can run independently and their combined resource use is acceptable. Promise.all does not move JavaScript computation to another thread, and an early rejection does not stop the other operation. For a large collection, starting everything at once is usually a poor substitute for a concurrency policy. I would also consider ordering of side effects, memory use, rate limits, cancellation, and whether partial results are useful before selecting a combinator.

119. How do you preserve useful error context without leaking sensitive information?

I would preserve the original failure as a cause when adding useful context at a boundary, then translate it into a safe public response at the application edge. Replacing every error with a generic string loses the chain needed for diagnosis.

JavaScript example

async function loadAccount(readRecord, id) {
  try {
    return await readRecord(id);
  } catch (cause) {
    throw new Error("Account lookup failed", { cause });
  }
}

async function loadAccount(readRecord, id) {
  try {
    return await readRecord(id);
  } catch (cause) {
    throw new Error("Account lookup failed", { cause });
  }
}

async function loadAccount(readRecord, id) {
  try {
    return await readRecord(id);
  } catch (cause) {
    throw new Error("Account lookup failed", { cause });
  }
}

The await is intentional: it allows this catch to observe a rejected readRecord promise. I would include a correlation identifier in structured logs, but avoid logging access tokens, raw secrets, or unrestricted request bodies. Error objects and causes can contain private implementation details, so they should not be blindly serialized to clients. I would log once at a meaningful boundary, classify retryable and expected failures, and ensure diagnostics do not become an additional failure or security path.

120. How would you review a JavaScript service for production readiness?

I would review explicit invariants and failure behavior before concentrating on syntax. The service should validate trust boundaries, enforce authorization, bound concurrency and queues, apply deadlines, release resources, and define what happens after partial success or process shutdown.

I would examine tests for domain logic, adapter contracts, cancellation, races, invalid input, and compatibility. Operational checks include structured error reporting, useful latency and saturation metrics, memory trends, dependency timeouts, and safe handling of secrets. Performance claims should come from representative measurements, not isolated microbenchmarks.

I would also inspect module boundaries, runtime and package assumptions, reproducible dependency installation, and deployment rollback. Node and browser execution require different operational checks, so a passing unit suite in one is not proof of correctness in the other. My final decision would identify remaining risks and evidence rather than claiming that static review or a high coverage percentage proves the system production-safe.

JavaScript Documentation and Further Reading

The stable language baseline is ECMAScript 2024, with browser and Node.js APIs labeled separately; proposals are not assumed. Safe pure-JavaScript fixtures were executed locally with Node v24.22.1. Browser DOM, rendering, security-policy, worker deployment, and live-network examples received source-based review rather than browser or production integration tests.

These 15 curated primary references cover the language specification, practical language semantics, browser scheduling and DOM behavior, Node.js, and security. Consult the documentation and compatibility requirements for the runtime used by your project.

Team Sherlock.sh

Building fraud intelligence for live high-stake video interactions (financial discussions, hiring interviews, board meeting etc).

Catch candidate fraud before it costs you.

Sherlock AI helps you detect AI-assisted, proxy, and deepfake candidates, so every hiring decision is backed by evidence.

Ask AI about Sherlock.sh

Similar Posts