JavaScript Questions & Answers
A focused companion to the handbook for testing recall and practicing concise interview explanations.
Choose a topic
Open a question for the concise answer, explanation, and deeper follow-up.
01JavaScript fundamentals17 questions
001
What is the difference between var, let, and const?
Short answervar,let, andconstdeclare variables, but they differ in scope, redeclaration, reassignment, and initialization.varis function-scoped and can be redeclared and reassigned.letis block-scoped, cannot be redeclared in the same scope, and can be reassigned.constis block-scoped and cannot be redeclared or reassigned.
Why it mattersPrefer
constby default and useletwhen a variable must change. Avoidvarin modern JavaScript because its function scope can cause unexpected behavior.const name = "Alex"; let count = 0; count++;More details
Variables declared with
letandconstbelong to the nearest block, whilevarbelongs to the nearest function:if (true) { var functionScoped = "available outside"; let blockScoped = "available only inside"; } console.log(functionScoped); // "available outside" console.log(blockScoped); // ReferenceErrorA
vardeclaration can be repeated in the same scope:var score = 10; var score = 20; // Allowedletandconstcannot be redeclared in the same scope:let age = 30; let age = 31; // SyntaxErrorconstprevents reassignment, but it does not make objects immutable:const user = { name: "Alex" }; user.name = "Sam"; // Allowed user = { name: "Lee" }; // TypeErrorAll three declarations are hoisted. However,
varis initialized withundefined, whileletandconstremain inaccessible in the temporal dead zone until their declarations execute.Finished reviewing this question?002
What are JavaScript’s primitive data types?
Short answerJavaScript has seven primitive data types:
stringnumberbigintbooleanundefinedsymbolnull
A primitive represents a single immutable value. Everything that is not a primitive is an object, including arrays and functions.
Why it mattersPrimitive values cannot be modified in place and are compared by value:
const first = "hello"; const second = "hello"; first === second; // trueMore details
Each primitive has a particular purpose:
stringrepresents text.numberrepresents floating-point numbers, includingNaNandInfinity.bigintrepresents integers larger than the safe range supported bynumber.booleanrepresentstrueorfalse.undefinedcommonly represents a missing or uninitialized value.symbolcreates unique identifiers.nullrepresents an intentional absence of a value.
const title = "Developer"; const price = 19.99; const largeNumber = 9007199254740993n; const isActive = true; const missing = undefined; const id = Symbol("id"); const selectedUser = null;Primitive values are immutable:
let text = "hello"; text.toUpperCase(); console.log(text); // "hello"String methods return new values rather than modifying the original string.
The
typeofoperator identifies most primitive types:typeof "hello"; // "string" typeof 42; // "number" typeof 42n; // "bigint" typeof true; // "boolean" typeof undefined; // "undefined" typeof Symbol(); // "symbol"One historical JavaScript behavior is:
typeof null; // "object"Despite this result,
nullis a primitive.Finished reviewing this question?003
What is the difference between null and undefined?
Short answerundefinedusually means that a value has not been assigned or does not exist.nullis an explicit value used to represent the intentional absence of a value.Why it mattersJavaScript frequently produces
undefinedautomatically, while developers normally assignnulldeliberately.let name; // undefined const user = null; // intentionally emptyMore details
Common situations that produce
undefinedinclude:- A declared but uninitialized variable
- A missing object property
- A function parameter that was not provided
- A function with no explicit return value
let value; console.log(value); // undefined const user = {}; console.log(user.name); // undefined function greet(name) { console.log(name); } greet(); // undefined function doSomething() {} doSomething(); // returns undefinednullis commonly used when a value is intentionally empty:const state = { selectedUser: null };Their types behave differently:
typeof undefined; // "undefined" typeof null; // "object"Loose equality considers them equal, but strict equality does not:
null == undefined; // true null === undefined; // falseA useful nullish check is:
if (value == null) { // Runs when value is either null or undefined }In most other comparisons, strict equality should be preferred.
Finished reviewing this question?004
What is the difference between == and ===?
Short answerThe loose equality operator
==compares values after performing type coercion when necessary. The strict equality operator===compares values without converting their types.Why it mattersStrict equality is more predictable and should generally be preferred:
5 == "5"; // true 5 === "5"; // falseMore details
Loose equality follows type-conversion rules that can create surprising results:
0 == false; // true "" == false; // true null == undefined; // true " \t" == 0; // trueStrict equality requires matching types:
0 === false; // false "" === false; // false null === undefined; // falseNaNis not equal to itself with either operator:NaN == NaN; // false NaN === NaN; // falseUse
Number.isNaN()to test for it:Number.isNaN(NaN); // trueObjects are compared by identity rather than by their contents:
{} === {}; // false const user = { name: "Alex" }; const sameUser = user; user === sameUser; // trueJavaScript also provides
Object.is(), which differs from===in two notable cases:Object.is(NaN, NaN); // true Object.is(0, -0); // falseFinished reviewing this question?005
What are truthy and falsy values?
Short answerA truthy value becomes
truewhen evaluated in a Boolean context. A falsy value becomesfalse.JavaScript’s falsy values are:
false0-00n""nullundefinedNaN
Every other value is truthy.
Why it mattersJavaScript performs Boolean coercion in conditions and logical expressions:
if ("hello") { // Runs because a non-empty string is truthy }More details
Values that sometimes look empty can still be truthy:
Boolean([]); // true Boolean({}); // true Boolean("false"); // true Boolean("0"); // trueLogical operators return operands rather than always returning Boolean values:
"hello" && 42; // 42 "" && 42; // "" "" || "guest"; // "guest"The
&&operator returns the first falsy operand or the final operand if all are truthy. The||operator returns the first truthy operand or the final operand if all are falsy.This can cause problems when
0or an empty string is a valid value:const count = 0; const result = count || 10; console.log(result); // 10Nullish coalescing only uses the fallback for
nullorundefined:const result = count ?? 10; console.log(result); // 0Use
Boolean(value)or double negation when an actual Boolean is required:Boolean("hello"); // true !!"hello"; // trueFinished reviewing this question?006
What is type coercion?
Short answerType coercion is the conversion of a value from one data type to another. It can happen automatically through implicit coercion or intentionally through explicit conversion.
Why it mattersJavaScript may automatically convert values when operators receive different types:
"5" + 2; // "52" "5" - 2; // 3Explicit conversion is usually easier to understand:
Number("5"); // 5 String(5); // "5" Boolean(0); // falseMore details
Implicit coercion depends on the operator and the values involved.
The
+operator performs addition when both operands are numbers. If either operand becomes a string, it performs string concatenation:5 + 2; // 7 "5" + 2; // "52" 5 + "2"; // "52"Most other arithmetic operators attempt to convert their operands to numbers:
"10" - 4; // 6 "10" * 2; // 20 "10" / 2; // 5If a value cannot be converted into a valid number, the result is
NaN:Number("hello"); // NaN "hello" * 2; // NaNBoolean coercion occurs in conditions and logical expressions:
Boolean(""); // false Boolean("false"); // true Boolean([]); // true Boolean({}); // trueLoose equality performs coercion before comparison, while strict equality does not:
5 == "5"; // true 5 === "5"; // falseExplicit conversion communicates intent more clearly and reduces unexpected behavior:
const input = "42"; const total = Number(input) + 8; console.log(total); // 50Finished reviewing this question?007
What is hoisting?
Short answerHoisting describes how JavaScript creates declarations before executing the code in a scope.
Different declarations behave differently:
- Function declarations are initialized with their function definitions.
varvariables are initialized withundefined.let,const, andclassdeclarations remain inaccessible until their declarations execute.- Function expressions follow the rules of the variables that contain them.
Why it mattersJavaScript does not physically move declarations. Hoisting describes the observable result of declarations being processed before code execution.
sayHello(); // Works function sayHello() { console.log("Hello"); }More details
A
varvariable can be accessed before its declaration, but its value is initiallyundefined:console.log(score); // undefined var score = 10;Conceptually, the declaration is processed before execution, but the assignment remains in place:
var score; console.log(score); score = 10;letandconstare also registered before execution, but accessing them early throws aReferenceError:console.log(age); // ReferenceError let age = 30;Function expressions are not fully initialized early:
greet(); // TypeError: greet is not a function var greet = function () { console.log("Hello"); };With
letorconst, calling the function expression early produces aReferenceError:greet(); // ReferenceError const greet = function () { console.log("Hello"); };Understanding hoisting helps explain why declaration style affects when a variable or function can be used.
Finished reviewing this question?008
What is the temporal dead zone?
Short answerThe temporal dead zone, or TDZ, is the period between entering a scope and executing the declaration of a
let,const, orclassbinding.During this period, the binding belongs to the scope but has not been initialized. Accessing it throws a
ReferenceError.Why it mattersThe TDZ ends when JavaScript reaches the declaration:
console.log(count); // ReferenceError let count = 1; console.log(count); // 1More details
A variable in the TDZ can also shadow a variable from an outer scope:
const status = "global"; { console.log(status); // ReferenceError const status = "local"; }JavaScript does not use the global
statusinside the block because the local binding belongs to that entire block. It remains inaccessible until its declaration.Even
typeof, which is normally safe for undeclared variables, throws an error for a variable in the TDZ:typeof unknownVariable; // "undefined" typeof value; // ReferenceError let value = 10;A
constvariable must be initialized at its declaration:const name; // SyntaxErrorA
letvariable can be declared without an initial value. It becomesundefinedafter the declaration executes:let name; console.log(name); // undefinedThe TDZ encourages variables to be declared before use and helps reveal initialization-order mistakes.
Finished reviewing this question?009
What is lexical scope?
Short answerLexical scope means that a variable’s accessibility is determined by where variables and functions are written in the source code.
When JavaScript resolves a variable, it searches the current scope and then moves outward through the surrounding scopes. This sequence is called the scope chain.
Why it mattersInner functions can access variables from outer scopes, but outer scopes cannot access variables declared only inside inner scopes.
const message = "Hello"; function greet() { console.log(message); // Accessible }More details
JavaScript has several kinds of scope:
- Global scope
- Module scope
- Function scope
- Block scope
A function retains the scope in which it was defined, regardless of where it is called:
const name = "Global"; function printName() { console.log(name); } function run() { const name = "Local"; printName(); } run(); // "Global"printNamewas defined in the global lexical environment, so it uses the globalname. The fact that it is called insiderundoes not change its scope.Nested functions can search through multiple outer scopes:
const globalValue = "global"; function outer() { const outerValue = "outer"; function inner() { const innerValue = "inner"; console.log(innerValue); console.log(outerValue); console.log(globalValue); } inner(); }Scope lookup moves outward only.
outercannot accessinnerValue.Lexical scope is also the foundation of closures because a function retains access to the environment where it was created.
Finished reviewing this question?010
What is a closure?
Short answerA closure is the combination of a function and the lexical environment in which that function was created.
It allows a function to retain access to variables from its outer scope even after the outer function has finished executing.
Why it mattersClosures are commonly used for private state, callbacks, function factories, memoization, and partial application.
function createCounter() { let count = 0; return () => ++count; }More details
The returned function continues to access
countaftercreateCounterhas completed:const counter = createCounter(); counter(); // 1 counter(); // 2 counter(); // 3Each invocation of the outer function creates a separate lexical environment:
const firstCounter = createCounter(); const secondCounter = createCounter(); firstCounter(); // 1 firstCounter(); // 2 secondCounter(); // 1Closures can create private state:
function createAccount(initialBalance) { let balance = initialBalance; return { deposit(amount) { balance += amount; }, getBalance() { return balance; } }; } const account = createAccount(100); account.deposit(50); account.getBalance(); // 150The
balancevariable cannot be accessed directly from outside the returned methods.Closures are also created in event handlers:
function attachHandler(button, message) { button.addEventListener("click", () => { console.log(message); }); }The handler retains access to
message.Closures may keep referenced data in memory for as long as the closure remains reachable. Unnecessary long-lived closures can therefore contribute to memory usage.
Finished reviewing this question?011
What is variable shadowing?
Short answerVariable shadowing occurs when an inner scope declares a variable with the same name as a variable in an outer scope.
Inside the inner scope, that name resolves to the inner variable. The outer variable still exists and becomes accessible again after execution leaves the inner scope.
Why it mattersShadowing is valid, but excessive shadowing can make code confusing because the same name represents different values in nearby scopes.
const name = "Alex"; function greet() { const name = "Sam"; console.log(name); // "Sam" }More details
Shadowing can occur in functions and blocks:
const status = "offline"; { const status = "online"; console.log(status); // "online" } console.log(status); // "offline"Function parameters can also shadow outer variables:
const user = "Alex"; function greet(user) { console.log(user); } greet("Sam"); // "Sam"Not every combination of declarations is allowed. For example, a
letvariable cannot be redeclared withvarin the same scope:let count = 1; var count = 2; // SyntaxErrorShadowing is different from reassignment:
let score = 10; { score = 20; // Reassigns the outer variable } console.log(score); // 20Declaring
let score = 20inside the block would create a separate shadowing variable instead.Clear, specific names are usually preferable when shadowing could make the code difficult to follow.
Finished reviewing this question?012
What is the difference between passing values by value and by reference?
Short answerJavaScript always passes function arguments by value.
- For a primitive, the copied value is the primitive itself.
- For an object, array, or function, the copied value is a reference identifying the same object.
A function can mutate an object through the copied reference, but reassigning the parameter does not change the caller’s variable.
Why it mattersJavaScript is accurately described as pass-by-value, including when the value being copied is an object reference.
function update(user) { user.name = "Sam"; user = { name: "Lee" }; }More details
Reassigning a primitive parameter does not affect the original variable:
function change(value) { value = 20; } let score = 10; change(score); console.log(score); // 10The function receives its own copy of the number.
With an object, the parameter and the caller’s variable initially contain copies of the same reference:
function update(user) { user.name = "Sam"; } const person = { name: "Alex" }; update(person); console.log(person.name); // "Sam"Both references identify the same object, so mutation is visible to the caller.
Reassigning the parameter only replaces the function’s local reference:
function replace(user) { user = { name: "Lee" }; } const person = { name: "Alex" }; replace(person); console.log(person.name); // "Alex"This distinction also applies to arrays:
function addItem(items) { items.push("new"); // Mutates the original array } function replaceItems(items) { items = []; // Reassigns only the local parameter }To avoid unwanted mutation, create a new object or array:
function rename(user, name) { return { ...user, name }; }Finished reviewing this question?013
How do shallow and deep copies differ?
Short answerA shallow copy creates a new top-level object or array but reuses references to nested objects. A deep copy creates independent copies of nested data as well.
Modifying a nested value in a shallow copy may affect the original. Modifying a deep copy does not.
Why it mattersSpread syntax,
Object.assign(),Array.from(), andslice()create shallow copies.structuredClone()can deeply copy many supported values.const copy = { ...original };More details
A shallow copy duplicates only the first level:
const original = { name: "Alex", settings: { theme: "light" } }; const copy = { ...original }; copy.name = "Sam"; copy.settings.theme = "dark"; console.log(original.name); // "Alex" console.log(original.settings.theme); // "dark"nameis independent because it is a primitive top-level property. The nestedsettingsobject remains shared.structuredClone()creates a deep copy:const deepCopy = structuredClone(original); deepCopy.settings.theme = "blue"; console.log(original.settings.theme); // "dark"It supports many types, including:
- Objects and arrays
DateMapandSet- Typed arrays
- Circular references
It cannot clone every value. Functions, DOM nodes, and some platform-specific objects are not supported.
Converting through JSON is sometimes used as a simple deep-copy technique:
const copy = JSON.parse(JSON.stringify(original));However, this approach loses or changes unsupported data such as
undefined, functions, symbols,Date,Map,Set,BigInt, and circular references. It should not be treated as a general-purpose cloning solution.Finished reviewing this question?014
What is destructuring?
Short answerDestructuring is syntax that extracts values from arrays or properties from objects and assigns them to variables.
Object destructuring matches values by property name, while array destructuring matches values by position.
Why it mattersDestructuring provides a concise way to access selected data:
const user = { name: "Alex", age: 30 }; const { name, age } = user; const colors = ["red", "blue"]; const [primary, secondary] = colors;More details
Object properties can be renamed:
const user = { name: "Alex" }; const { name: displayName } = user; console.log(displayName); // "Alex"Default values are used when the extracted value is
undefined:const user = {}; const { role = "guest" } = user; console.log(role); // "guest"A default does not replace
null:const user = { role: null }; const { role = "guest" } = user; console.log(role); // nullArray elements can be skipped:
const colors = ["red", "green", "blue"]; const [primary, , tertiary] = colors; console.log(tertiary); // "blue"Nested values can be destructured:
const user = { profile: { email: "alex@example.com" } }; const { profile: { email } } = user;Rest syntax collects the remaining values:
const [first, ...remaining] = [1, 2, 3, 4]; // first: 1 // remaining: [2, 3, 4]Destructuring is also useful in function parameters:
function printUser({ name, role = "guest" }) { console.log(`${name}: ${role}`); }Nested destructuring can throw an error if an expected parent value is missing, so defaults or optional access may be necessary when working with uncertain data.
Finished reviewing this question?015
What are rest and spread syntax, and how do they differ?
Short answerRest and spread both use
..., but they perform opposite operations:- Spread expands an iterable or object into individual values or properties.
- Rest collects multiple remaining values or properties into a new array or object.
Their meaning is determined by where the syntax appears.
Why it mattersSpread expands values, while rest collects them:
const numbers = [1, 2, 3]; const copy = [...numbers]; // Spread function sum(...values) { // Rest return values.reduce((total, value) => total + value, 0); }More details
Array spread can copy or combine arrays:
const first = [1, 2]; const second = [3, 4]; const combined = [...first, ...second]; // [1, 2, 3, 4]Function-call spread passes iterable values as separate arguments:
const numbers = [4, 8, 2]; Math.max(...numbers); // 8Object spread copies enumerable own properties:
const defaults = { theme: "light", language: "en" }; const settings = { ...defaults, theme: "dark" };Later properties override earlier properties, so
settings.themebecomes"dark".Rest parameters collect arguments into a real array:
function average(...numbers) { const total = numbers.reduce( (sum, number) => sum + number, 0 ); return total / numbers.length; }Rest syntax can collect remaining destructured values:
const user = { id: 1, name: "Alex", role: "admin" }; const { id, ...details } = user;Spread creates shallow copies. Nested objects remain shared:
const original = { settings: { dark: false } }; const copy = { ...original }; copy.settings.dark = true; console.log(original.settings.dark); // trueFinished reviewing this question?016
What are template literals?
Short answerTemplate literals are strings enclosed by backticks.
They support:
- Expression interpolation with
${expression} - Multiline strings
- Tagged templates that process the literal through a function
Why it mattersTemplate literals make dynamic strings more readable:
const name = "Alex"; const message = `Hello, ${name}!`;More details
Any valid expression can appear inside
${...}:const price = 20; const quantity = 3; const message = `Total: $${price * quantity}`; // "Total: $60"Properties and function calls can also be interpolated:
const user = { name: "Alex" }; const text = `Welcome, ${user.name.toUpperCase()}!`;Template literals can span multiple lines:
const content = `First line Second line Third line`;Tagged templates pass the literal’s static strings and evaluated expressions to a function:
function inspect(strings, ...values) { console.log(strings); console.log(values); } const name = "Alex"; const count = 3; inspect`${name} has ${count} messages`;Tagged templates can be used for escaping content, localization, formatting, or domain-specific syntax.
Interpolation does not automatically make untrusted content safe. When inserting user-controlled values into HTML, developers must still use appropriate escaping or safe DOM APIs to prevent cross-site scripting.
Finished reviewing this question?- Expression interpolation with
017
What are JavaScript modules, and how do import and export work?
Short answerJavaScript modules divide an application into separate files with explicit dependencies and their own top-level scope.
exportexposes values from a module, andimportmakes those values available in another module.A module may have multiple named exports but only one default export.
Why it mattersModules improve code organization, reuse, dependency management, and encapsulation.
// math.js export const add = (a, b) => a + b; // app.js import { add } from "./math.js";More details
Named exports use the exported names:
// math.js export const add = (a, b) => a + b; export const subtract = (a, b) => a - b;They are imported with braces:
import { add, subtract } from "./math.js";A named import can be aliased:
import { add as sum } from "./math.js";A default export can be imported using any local name:
// logger.js export default function log(message) { console.log(message); }import writeLog from "./logger.js";Named and default imports can be combined:
// math.js export default function multiply(a, b) { return a * b; } export const add = (a, b) => a + b;import multiply, { add } from "./math.js";Static imports are resolved before the module executes and normally appear at the top level. Dynamic
import()loads a module asynchronously:const module = await import("./analytics.js"); module.trackPageView();Dynamic imports are useful for conditional loading and code splitting.
Imported bindings are live bindings. If the exporting module updates an exported variable, importing modules observe its current value:
// counter.js export let count = 0; export function increment() { count++; }Modules are evaluated once, and repeated imports reuse the same module instance.
In browsers, modules are loaded with:
<script type="module" src="./app.js"></script>Browser modules use strict mode automatically, have their own scope, and are deferred by default.
Finished reviewing this question?
02Functions and objects15 questions
018
What is the difference between a function declaration and a function expression?
Short answerA function declaration defines a named function as a standalone statement. A function expression creates a function as part of an expression and usually assigns it to a variable.
Function declarations are fully initialized during hoisting. Function expressions follow the initialization rules of the variables that contain them.
Why it mattersA function declaration can be called before it appears in the code. A function expression normally cannot.
greet(); // Works function greet() { console.log("Hello"); }More details
A function declaration uses this syntax:
function add(a, b) { return a + b; }A function expression can be anonymous:
const add = function (a, b) { return a + b; };It can also have an internal name:
const calculate = function add(a, b) { return a + b; };When a function expression is stored in
const, the variable is in the temporal dead zone before its declaration:greet(); // ReferenceError const greet = function () { console.log("Hello"); };With
var, the variable is initialized withundefined, so calling it early produces aTypeError:greet(); // TypeError: greet is not a function var greet = function () { console.log("Hello"); };Function expressions are useful when functions are passed as values, used as callbacks, or created conditionally.
Finished reviewing this question?019
How do arrow functions differ from regular functions?
Short answerArrow functions provide shorter syntax and do not create their own
this,arguments,super, ornew.target.Their
thisvalue is inherited lexically from the surrounding scope. Arrow functions also cannot be used as constructors and do not have aprototypeproperty for constructing instances.Why it mattersArrow functions are useful for callbacks and functions that should preserve the surrounding
this. Regular functions are usually more appropriate for object methods and constructors.const double = number => number * 2;More details
Arrow functions can use an implicit return when the body contains one expression:
const add = (a, b) => a + b;Curly braces require an explicit
return:const add = (a, b) => { return a + b; };Returning an object implicitly requires parentheses:
const createUser = name => ({ name });Arrow functions inherit
thisfrom their surrounding scope:const timer = { seconds: 0, start() { setInterval(() => { this.seconds++; }, 1000); } };The callback uses the same
thisasstart.An arrow function is usually unsuitable as an object method when dynamic
thisis required:const user = { name: "Alex", greet: () => { console.log(this.name); } };The arrow function does not receive
useras itsthis.Arrow functions cannot be called with
new:const User = name => { this.name = name; }; new User("Alex"); // TypeErrorThey also do not have their own
argumentsobject. Rest parameters should be used instead:const sum = (...numbers) => numbers.reduce((total, number) => total + number, 0);Finished reviewing this question?020
How does the this keyword work?
Short answerIn a regular function,
thisis usually determined by how the function is called, not where it is defined.Its value can come from:
- A method call
- A constructor call with
new - An explicit call using
call,apply, orbind - The default function-calling rule
- The surrounding lexical scope for an arrow function
Why it mattersWhen a function is called as an object method,
thisnormally refers to the object before the dot.const user = { name: "Alex", greet() { console.log(this.name); } }; user.greet(); // "Alex"More details
A standalone function call uses the default binding:
"use strict"; function showThis() { console.log(this); } showThis(); // undefinedIn older non-strict browser scripts, the default value may be the global object.
A method can lose its receiver when it is separated from its object:
const greet = user.greet; greet(); // `this` is no longer `user`Explicit binding can provide a value:
greet.call(user); // "Alex"Constructor calls create a new object and bind it to
this:function User(name) { this.name = name; } const user = new User("Alex");Arrow functions do not create their own
this:const user = { name: "Alex", delayedGreeting() { setTimeout(() => { console.log(this.name); }, 100); } };Binding precedence is generally:
- Constructor binding with
new - Explicit binding with
bind,call, orapply - Method binding
- Default binding
A function created by
bindkeeps its boundthis, although calling that bound function withnewuses the newly created instance.Finished reviewing this question?021
What do call, apply, and bind do?
Short answercall,apply, andbindlet developers control thethisvalue used by a regular function.callinvokes the function immediately with arguments listed separately.applyinvokes the function immediately with arguments provided as an array or array-like value.bindreturns a new function with a fixedthisvalue and optional preset arguments.
Why it matterscallandapplyexecute a function immediately.bindcreates a function that can be executed later.greet.call(user, "Hello"); greet.apply(user, ["Hello"]); const boundGreet = greet.bind(user);More details
Consider this function:
function introduce(greeting, punctuation) { return `${greeting}, I am ${this.name}${punctuation}`; } const user = { name: "Alex" };callaccepts individual arguments:introduce.call(user, "Hello", "!"); // "Hello, I am Alex!"applyaccepts them as a collection:introduce.apply(user, ["Hello", "!"]); // "Hello, I am Alex!"bindcreates a new function:const introduceAlex = introduce.bind(user); introduceAlex("Hello", "!"); // "Hello, I am Alex!"bindcan also preset arguments, which is called partial application:const sayHelloToAlex = introduce.bind( user, "Hello" ); sayHelloToAlex("!");These methods cannot replace the lexical
thisof an arrow function:const showThis = () => this; showThis.call(user); // Does not change arrow `this`Finished reviewing this question?022
What is a higher-order function?
Short answerA higher-order function is a function that accepts one or more functions as arguments, returns a function, or does both.
Higher-order functions support composition and abstraction by allowing behavior to be treated as data.
Why it mattersCommon array methods such as
map,filter, andreduceare higher-order functions because they receive callback functions.const doubled = [1, 2, 3].map( number => number * 2 );More details
A function that receives another function is higher-order:
function repeat(times, action) { for (let index = 0; index < times; index++) { action(index); } } repeat(3, index => { console.log(index); });A function that returns a function is also higher-order:
function multiplyBy(multiplier) { return number => number * multiplier; } const double = multiplyBy(2); const triple = multiplyBy(3); double(5); // 10 triple(5); // 15Higher-order functions can separate reusable control flow from changing behavior:
function withLogging(action) { return (...argumentsList) => { console.log("Starting"); const result = action(...argumentsList); console.log("Finished"); return result; }; }This pattern is useful for validation, authorization, caching, logging, retries, and event handling.
Finished reviewing this question?023
What is a callback function?
Short answerA callback is a function passed to another function so that the receiving function can execute it at an appropriate time.
Callbacks may run synchronously, such as callbacks passed to
map, or asynchronously, such as event handlers and timer callbacks.Why it mattersA callback allows one function to receive customizable behavior.
function processUser(user, callback) { callback(user); } processUser({ name: "Alex" }, user => { console.log(user.name); });More details
Synchronous callbacks execute before the surrounding function returns:
const numbers = [1, 2, 3]; const doubled = numbers.map(number => { return number * 2; });Asynchronous callbacks execute later:
setTimeout(() => { console.log("Executed later"); }, 1000);Event listeners also use callbacks:
button.addEventListener("click", event => { console.log(event.target); });Traditional asynchronous code can become deeply nested:
getUser(userId, user => { getOrders(user.id, orders => { getOrderDetails(orders[0].id, details => { console.log(details); }); }); });This is often called callback hell. Promises and
async/awaitmake many asynchronous workflows easier to compose and handle.Error-first callbacks are common in older Node.js APIs:
operation((error, result) => { if (error) { console.error(error); return; } console.log(result); });Finished reviewing this question?024
What are pure functions and side effects?
Short answerA pure function always returns the same output for the same inputs and does not modify or depend on external mutable state.
A side effect is any observable interaction with something outside the function’s returned value.
Why it mattersPure functions are easier to test, reuse, cache, and reason about.
function add(a, b) { return a + b; }More details
Common side effects include:
- Modifying an external variable
- Mutating an argument
- Updating the DOM
- Writing to storage
- Sending a network request
- Logging to the console
- Reading the current time
- Generating a random value
This function is impure because it changes external state:
let total = 0; function addToTotal(amount) { total += amount; return total; }This version is pure:
function addToTotal(total, amount) { return total + amount; }Mutation can also make a function impure:
function addUser(users, user) { users.push(user); return users; }A non-mutating version returns a new array:
function addUser(users, user) { return [...users, user]; }Real applications require side effects. The goal is usually to isolate them from pure business logic rather than eliminate them entirely.
For example, data transformation can remain pure while a separate function handles the network request or DOM update.
Finished reviewing this question?025
What is an immediately invoked function expression (IIFE)?
Short answerAn IIFE is a function expression that executes immediately after it is created.
It was traditionally used to create a private scope and prevent variables from leaking into the global scope.
Why it mattersParentheses turn the function declaration-like syntax into an expression, and the final parentheses invoke it.
(function () { const message = "Private"; console.log(message); })();More details
An arrow function can also be used:
(() => { const value = 42; console.log(value); })();An IIFE can return a value:
const result = (() => { const first = 10; const second = 20; return first + second; })(); console.log(result); // 30IIFEs were commonly used in the module pattern:
const counter = (() => { let count = 0; return { increment() { count++; }, getCount() { return count; } }; })();The
countvariable remains private.Modern JavaScript modules and block-scoped variables have reduced the need for IIFEs. However, they can still be useful for one-time initialization or creating an isolated scope in older code.
An async IIFE allows
awaitin environments where top-levelawaitis unavailable:(async () => { const response = await fetch("/api/users"); const users = await response.json(); console.log(users); })();Finished reviewing this question?026
How do default parameters work?
Short answerDefault parameters provide fallback values for function parameters when the corresponding argument is missing or explicitly set to
undefined.The default expression is evaluated when the function is called, not when it is defined.
Why it mattersA default value is not used when the argument is
null,false,0, or an empty string.function greet(name = "Guest") { return `Hello, ${name}`; } greet(); // "Hello, Guest" greet(undefined); // "Hello, Guest" greet(null); // "Hello, null"More details
Default expressions can reference earlier parameters:
function createRange(start, end = start + 10) { return { start, end }; } createRange(5); // { start: 5, end: 15 }A later parameter cannot safely be referenced before it has been initialized:
function example(first = second, second = 2) { return first + second; } example(); // ReferenceErrorDefault expressions can call functions:
function generateId() { return crypto.randomUUID(); } function createUser(name, id = generateId()) { return { name, id }; }generateId()runs each time the default is needed.Destructured parameters can have defaults:
function configure({ theme = "light", language = "en" } = {}) { return { theme, language }; } configure(); // Works because the object defaults to {}Without
= {}, callingconfigure()would attempt to destructureundefinedand throw an error.Finished reviewing this question?027
How do JavaScript objects inherit properties?
Short answerJavaScript objects inherit properties and methods through prototypes.
Every ordinary object has an internal prototype link to another object or to
null. When a property is not found directly on an object, JavaScript follows this link through the prototype chain.Why it mattersInheritance allows multiple objects to share behavior without copying the same methods onto every instance.
const animal = { speak() { console.log("Sound"); } }; const dog = Object.create(animal); dog.speak();More details
An object can have both own properties and inherited properties:
const animal = { type: "animal" }; const dog = Object.create(animal); dog.name = "Rex"; dog.name; // Own property dog.type; // Inherited propertyObject.hasOwn()checks whether a property belongs directly to the object:Object.hasOwn(dog, "name"); // true Object.hasOwn(dog, "type"); // falseConstructor functions typically place shared methods on their
prototypeobject:function User(name) { this.name = name; } User.prototype.greet = function () { return `Hello, ${this.name}`; }; const user = new User("Alex"); user.greet();The
newoperator creates an object whose prototype points toUser.prototype.Classes use the same prototype-based mechanism:
class User { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}`; } }Class syntax makes the pattern more readable but does not replace prototype-based inheritance with a separate inheritance model.
Finished reviewing this question?028
What is the prototype chain?
Short answerThe prototype chain is the sequence of objects JavaScript searches when resolving a property.
JavaScript first checks the object itself. If the property is not found, it checks the object’s prototype and continues upward until it finds the property or reaches
null.Why it mattersThe prototype chain enables inherited properties and shared methods.
const user = {}; user.toString();toStringis found throughObject.prototype.More details
Consider an array:
const numbers = [1, 2, 3];A simplified version of its prototype chain is:
numbers → Array.prototype → Object.prototype → nullnumbers.mapis found onArray.prototype, whilenumbers.toStringcan ultimately be found through the chain.An own property shadows an inherited property with the same name:
const parent = { role: "parent" }; const child = Object.create(parent); child.role = "child"; console.log(child.role); // "child"The prototype can be inspected using:
Object.getPrototypeOf(child);An object without
Object.prototypecan be created with:const dictionary = Object.create(null);This object does not inherit methods such as
toStringorhasOwnProperty.Directly changing prototypes with
Object.setPrototypeOf()can harm performance. Prototypes are usually established when objects are created through classes, constructor functions, object literals, orObject.create().Finished reviewing this question?029
What is the difference between a class and constructor function?
Short answerClasses and constructor functions can both create objects that share methods through prototypes.
JavaScript classes provide clearer syntax for constructors, instance methods, static methods, inheritance, and private fields. Internally, they still use prototype-based inheritance.
Why it mattersClasses are primarily a more structured syntax over JavaScript’s existing constructor and prototype mechanisms.
class User { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}`; } }More details
The constructor-function equivalent is:
function User(name) { this.name = name; } User.prototype.greet = function () { return `Hello, ${this.name}`; };Both versions allow this:
const user = new User("Alex"); user.greet();Important differences include:
- Class declarations are in the temporal dead zone.
- Class constructors must be called with
new. - Class bodies run in strict mode.
- Class methods are non-enumerable.
- Classes support
extends,super, static members, and private fields with concise syntax.
Calling a class without
newthrows an error:User("Alex"); // TypeErrorA traditional constructor function may be called without
new, although doing so is usually a bug.Classes support private fields:
class Account { #balance = 0; deposit(amount) { this.#balance += amount; } getBalance() { return this.#balance; } }Private fields cannot be accessed directly outside the class.
Finished reviewing this question?030
What is the difference between static and instance methods?
Short answerAn instance method is called on an object created by a class and can access that instance through
this.A static method belongs to the class itself rather than to individual instances. It is called using the class name.
Why it mattersInstance methods operate on instance-specific data. Static methods usually provide utilities or class-level behavior.
class User { greet() { return "Hello"; } static createGuest() { return new User(); } }More details
Instance methods are stored on the class prototype:
class User { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}`; } } const user = new User("Alex"); user.greet();The instance method is shared rather than recreated for every instance:
user.greet === User.prototype.greet; // trueStatic methods are called on the class:
class User { static fromJSON(json) { const data = JSON.parse(json); return new User(data.name); } constructor(name) { this.name = name; } } const user = User.fromJSON('{"name":"Alex"}');An instance does not inherit static methods:
user.fromJSON(); // TypeErrorInside a static method,
thisnormally refers to the class on which the method was called:class Base { static create() { return new this(); } } class Child extends Base {} Child.create(); // Creates a Child instanceStatic properties are also useful for class-level constants or shared state, although shared mutable state should be used carefully.
Finished reviewing this question?031
What are getters and setters?
Short answerGetters and setters are methods that control how an object property is read or assigned while allowing it to be used with ordinary property syntax.
A getter runs when the property is read. A setter runs when a value is assigned to the property.
Why it mattersGetters can compute values, while setters can validate or normalize assignments.
const user = { firstName: "Alex", lastName: "Smith", get fullName() { return `${this.firstName} ${this.lastName}`; } };More details
The getter is accessed like a property, not called like a method:
user.fullName; // "Alex Smith"A setter can validate data:
class Temperature { #celsius = 0; get celsius() { return this.#celsius; } set celsius(value) { if (!Number.isFinite(value)) { throw new TypeError("Temperature must be a number"); } this.#celsius = value; } }It is used with assignment syntax:
const temperature = new Temperature(); temperature.celsius = 25; console.log(temperature.celsius); // 25A getter can expose a derived value:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; } get area() { return this.width * this.height; } }Getters should generally avoid surprising side effects because property access looks like a simple read.
Setters accept exactly one parameter. If only a getter is defined, the property is read-only through that accessor.
Accessors can be defined in object literals, classes, or with
Object.defineProperty().Finished reviewing this question?032
What is the difference between mutable and immutable operations?
Short answerA mutable operation changes an existing object or array. An immutable operation creates and returns a new value without changing the original.
JavaScript primitives are immutable, but objects and arrays are mutable by default.
Why it mattersImmutable updates make state changes easier to track and are especially useful in frontend state management.
const original = [1, 2]; const updated = [...original, 3];More details
Common mutating array methods include:
pushpopshiftunshiftsplicesortreversefill
const numbers = [3, 1, 2]; numbers.sort(); console.log(numbers); // [1, 2, 3]Common non-mutating array methods include:
mapfiltersliceconcattoSortedtoReversedtoSpliced
const numbers = [3, 1, 2]; const sorted = numbers.toSorted(); console.log(numbers); // [3, 1, 2] console.log(sorted); // [1, 2, 3]Objects can be updated immutably with spread syntax:
const user = { name: "Alex", role: "user" }; const updatedUser = { ...user, role: "admin" };Nested updates require copying every changed level:
const state = { user: { settings: { theme: "light" } } }; const updatedState = { ...state, user: { ...state.user, settings: { ...state.user.settings, theme: "dark" } } };constdoes not make an object immutable:const user = { name: "Alex" }; user.name = "Sam"; // AllowedObject.freeze()prevents direct changes to an object’s own properties, but it is shallow:const settings = Object.freeze({ nested: { theme: "light" } }); settings.nested.theme = "dark"; // Nested object is not frozenImmutability can improve predictability, change detection, undo functionality, memoization, and debugging. However, copying large structures also has a performance and memory cost, so the appropriate strategy depends on the application.
Finished reviewing this question?
03Arrays and collections9 questions
033
What is the difference between map, filter, and reduce?
Short answermap,filter, andreduceiterate over arrays without modifying the original array by themselves.maptransforms every element and returns an array of the same length.filterkeeps elements that pass a condition and returns an array of equal or shorter length.reducecombines all elements into a single accumulated result.
Why it mattersChoose the method based on the desired result:
const numbers = [1, 2, 3, 4]; numbers.map(number => number * 2); // [2, 4, 6, 8] numbers.filter(number => number % 2 === 0); // [2, 4] numbers.reduce((sum, number) => sum + number, 0); // 10More details
mapcalls its callback for every existing element and stores each returned value:const users = [ { id: 1, name: "Alex" }, { id: 2, name: "Sam" } ]; const names = users.map(user => user.name); // ["Alex", "Sam"]filterincludes an element when the callback returns a truthy value:const activeUsers = users.filter(user => user.active);reducereceives an accumulator, the current value, the current index, and the original array:const total = numbers.reduce( (accumulator, number) => accumulator + number, 0 );The second argument is the accumulator’s initial value. Providing it is generally safer:
[].reduce((sum, number) => sum + number); // TypeError [].reduce((sum, number) => sum + number, 0); // 0reducecan build arrays, objects, maps, or other structures:const grouped = users.reduce((groups, user) => { const role = user.role; groups[role] ??= []; groups[role].push(user); return groups; }, {});Although these methods do not mutate the source array directly, their callbacks can still mutate objects contained in the array. Avoid such side effects when predictable transformations are important.
Finished reviewing this question?034
What is the difference between forEach and map?
Short answerBoth methods execute a callback for each array element.
mapreturns a new array containing the callback’s return values.forEachreturnsundefinedand is intended for side effects.
Why it mattersUse
mapto transform data. UseforEachwhen performing an action such as logging, updating the DOM, or modifying external state.const numbers = [1, 2, 3]; const doubled = numbers.map(number => number * 2); // [2, 4, 6] numbers.forEach(number => console.log(number));More details
Returning a value from a
forEachcallback does not create a result array:const result = [1, 2, 3].forEach(number => { return number * 2; }); console.log(result); // undefinedBoth methods receive the same callback arguments:
array.map((element, index, originalArray) => { // ... }); array.forEach((element, index, originalArray) => { // ... });Neither method supports early termination with
breakorreturn:[1, 2, 3].forEach(number => { if (number === 2) { return; // Skips only the rest of this callback } });Use
for...of,find,some, oreverywhen early termination is required.Avoid using
forEachdirectly with asynchronous callbacks when the operations must be awaited:// Does not wait for all callbacks to finish users.forEach(async user => { await saveUser(user); });Sequential processing can use
for...of:for (const user of users) { await saveUser(user); }Parallel processing can use
mapwithPromise.all:await Promise.all( users.map(user => saveUser(user)) );Finished reviewing this question?035
What is the difference between find and filter?
Short answerBoth methods test array elements using a callback.
findreturns the first matching element and stops searching.filterreturns a new array containing every matching element.
If no element matches,
findreturnsundefined, whilefilterreturns an empty array.Why it mattersUse
findwhen expecting one result andfilterwhen collecting multiple results.const numbers = [1, 2, 3, 4]; numbers.find(number => number > 2); // 3 numbers.filter(number => number > 2); // [3, 4]More details
findreturns the element itself, not its index:const users = [ { id: 1, name: "Alex" }, { id: 2, name: "Sam" } ]; const user = users.find(user => user.id === 2); // { id: 2, name: "Sam" }Use
findIndexwhen the position is needed:const index = users.findIndex(user => user.id === 2); // 1Newer JavaScript also provides
findLastandfindLastIndexfor searching from the end:const values = [1, 4, 2, 4]; values.findLast(value => value === 4); // 4 values.findLastIndex(value => value === 4); // 3filteralways returns a new array, even when nothing matches:const matches = users.filter(user => user.id === 99); console.log(matches); // [] console.log(Boolean(matches)); // trueBecause an empty array is truthy, check its length when determining whether results exist:
if (matches.length === 0) { console.log("No users found"); }Finished reviewing this question?036
What is the difference between some and every?
Short answersomeandeverytest array elements and return Boolean values.somereturnstruewhen at least one element passes the test.everyreturnstrueonly when all elements pass the test.
Both methods stop as soon as the final result is known.
Why it matterssomeanswers “Does any element match?” whileeveryanswers “Do all elements match?”const numbers = [2, 4, 5]; numbers.some(number => number % 2 !== 0); // true numbers.every(number => number > 0); // trueMore details
somestops after finding the first truthy callback result:const hasAdmin = users.some(user => { return user.role === "admin"; });everystops after finding the first falsy result:const allActive = users.every(user => { return user.active; });Their results for an empty array are important:
[].some(() => true); // false [].every(() => false); // truesomereturnsfalsebecause no element satisfies the condition.everyreturnstruebecause no element violates the condition. This is known as vacuous truth.somecan be used as an existence check:const hasPermission = permissions.some( permission => permission === "edit" );everyis useful for validation:const formIsValid = fields.every(field => { return field.value.trim() !== ""; });Because both short-circuit, they may be more efficient than
filterwhen only a Boolean result is required.Finished reviewing this question?037
Which array methods mutate the original array?
Short answerCommon mutating array methods include:
pushpopshiftunshiftsplicesortreversefillcopyWithin
Methods such as
map,filter,slice,concat,toSorted,toReversed, andtoSplicedreturn new arrays instead.Why it mattersMutation changes the existing array reference, which can create unexpected state changes in frontend applications.
const numbers = [3, 1, 2]; numbers.sort(); console.log(numbers); // [1, 2, 3]More details
Methods that add or remove elements mutate the array:
const items = ["a", "b"]; items.push("c"); // ["a", "b", "c"] items.pop(); // ["a", "b"] items.unshift("z"); // ["z", "a", "b"] items.shift(); // ["a", "b"]splicecan remove, replace, or insert elements:const items = ["a", "b", "c"]; items.splice(1, 1, "x"); // items is ["a", "x", "c"]sortandreversemutate the original:const numbers = [3, 1, 2]; const result = numbers.sort(); result === numbers; // trueModern non-mutating alternatives include:
const sorted = numbers.toSorted(); const reversed = numbers.toReversed(); const changed = numbers.toSpliced(1, 1, 10); const replaced = numbers.with(0, 99);Older code can copy before applying a mutating method:
const sorted = [...numbers].sort((a, b) => a - b);Assignment by index also mutates an array:
numbers[0] = 100;A method can return a new array while its callback still mutates contained objects:
const users = [{ name: "Alex" }]; const result = users.map(user => { user.name = "Sam"; return user; }); console.log(users[0].name); // "Sam"Finished reviewing this question?038
How can you remove duplicate values from an array?
Short answerFor primitive values, the most common solution is to create a
Setand convert it back into an array.A
Setstores only unique values and preserves insertion order.Why it mattersSpread syntax provides a concise solution:
const values = [1, 2, 2, 3, 3]; const uniqueValues = [...new Set(values)]; // [1, 2, 3]More details
Array.from()can also convert aSetinto an array:const uniqueValues = Array.from(new Set(values));Setworks well for duplicate primitive values:[...new Set(["a", "a", "b"])]; // ["a", "b"]Object values are compared by identity, not by contents:
const users = [ { id: 1 }, { id: 1 } ]; [...new Set(users)].length; // 2Although the objects contain the same data, they are different object references.
Objects can be deduplicated by a property using a
Map:const users = [ { id: 1, name: "Alex" }, { id: 2, name: "Sam" }, { id: 1, name: "Alexander" } ]; const uniqueUsers = [ ...new Map( users.map(user => [user.id, user]) ).values() ];Because later entries replace earlier entries with the same key, this keeps the final object for each
id.To keep the first occurrence:
const seen = new Set(); const uniqueUsers = users.filter(user => { if (seen.has(user.id)) { return false; } seen.add(user.id); return true; });The right solution depends on whether uniqueness is based on primitive equality, object identity, or a selected property.
Finished reviewing this question?039
When would you use Map instead of an object?
Short answerUse
Mapwhen keys are dynamic, keys are not limited to strings and symbols, insertion order matters, or the collection is frequently updated and iterated.Use an object when representing a record with known property names, especially when the data naturally maps to JSON.
Why it mattersA
Mapaccepts values of any type as keys and provides dedicated collection methods.const settings = new Map(); settings.set("theme", "dark"); settings.set(42, "numeric key"); settings.set(document.body, "element key");More details
Object property keys are strings or symbols. Other values are converted to strings:
const object = {}; object[1] = "number"; object["1"] = "string"; console.log(object[1]); // "string"A
Mapdistinguishes these keys:const map = new Map(); map.set(1, "number"); map.set("1", "string"); map.get(1); // "number" map.get("1"); // "string"Objects can also be used as keys:
const user = { id: 1 }; const permissions = new Map(); permissions.set(user, ["read", "write"]); permissions.get(user); // ["read", "write"]A
Mapprovides:map.set(key, value); map.get(key); map.has(key); map.delete(key); map.clear(); map.size;It is directly iterable:
for (const [key, value] of map) { console.log(key, value); }Objects are often more convenient for structured records:
const user = { id: 1, name: "Alex", role: "admin" };Objects work directly with
JSON.stringify(). AMaprequires explicit conversion before JSON serialization.Finished reviewing this question?040
When would you use Set?
Short answerUse a
Setwhen a collection should contain only unique values or when efficient membership checks, additions, and deletions are important.A
Setpreserves insertion order and can contain values of any type.Why it mattersA
Setis useful for deduplication and checking whether a value has already been seen.const selectedIds = new Set(); selectedIds.add(1); selectedIds.add(1); selectedIds.add(2); console.log(selectedIds.size); // 2More details
The main
Setoperations are:const roles = new Set(); roles.add("admin"); roles.has("admin"); // true roles.delete("admin"); // true roles.clear(); roles.size; // 0A
Setcan be constructed from any iterable:const uniqueLetters = new Set("hello"); // Set { "h", "e", "l", "o" }It can be converted into an array:
const values = [...uniqueLetters];Sets are directly iterable:
for (const role of roles) { console.log(role); }Object values are unique by identity:
const first = { id: 1 }; const second = { id: 1 }; const set = new Set([first, second]); console.log(set.size); // 2A
Setis often clearer than repeatedly usingarray.includes()when many membership checks are required:const allowedRoles = new Set([ "admin", "editor", "author" ]); if (allowedRoles.has(user.role)) { // Allow access }Modern JavaScript environments also provide set-composition methods such as
union,intersection,difference, andisSubsetOf. Compatibility should be checked when supporting older browsers.Finished reviewing this question?041
What are WeakMap and WeakSet?
Short answerWeakMapandWeakSetare collections that hold their object entries weakly. Their presence in the collection does not prevent those objects from being garbage-collected when no other reachable references remain.A
WeakMapassociates values with object or non-registered symbol keys. AWeakSettracks object or non-registered symbol values.Why it mattersThey are useful for attaching metadata or tracking objects without extending those objects’ lifetimes.
const metadata = new WeakMap(); const element = document.querySelector("button"); metadata.set(element, { clicks: 0 });More details
A regular
Mapstrongly retains its keys:const cache = new Map(); let user = { id: 1 }; cache.set(user, "cached"); user = null;The object remains reachable through
cache, so it cannot be garbage-collected.A
WeakMapdoes not keep its keys alive:const cache = new WeakMap(); let user = { id: 1 }; cache.set(user, "cached"); user = null;If no other references remain, the object and its associated entry may be collected.
Common
WeakMapoperations are:weakMap.set(object, value); weakMap.get(object); weakMap.has(object); weakMap.delete(object);Common
WeakSetoperations are:weakSet.add(object); weakSet.has(object); weakSet.delete(object);Weak collections are not iterable and do not expose
sizeorclear. Garbage collection is unpredictable, so JavaScript cannot provide a reliable list of their current entries.A
WeakMapcan store private metadata associated with an object:const privateData = new WeakMap(); class User { constructor(name) { privateData.set(this, { name }); } getName() { return privateData.get(this).name; } }A
WeakSetcan track whether particular objects have been processed:const processed = new WeakSet(); function process(object) { if (processed.has(object)) { return; } processed.add(object); // Process the object }Weak collections are specialized tools. Use
MaporSetwhen iteration, primitive keys, or collection size is required.Finished reviewing this question?
04Asynchronous JavaScript12 questions
042
What is the JavaScript event loop?
Short answerThe event loop coordinates synchronous JavaScript execution with asynchronous work.
It monitors the call stack and task queues. When the call stack is empty, it allows queued callbacks to run according to their priority and scheduling rules.
Why it mattersJavaScript executes one piece of JavaScript code at a time on its main thread, but browser APIs can perform operations such as timers and network requests outside the call stack.
console.log("First"); setTimeout(() => { console.log("Third"); }, 0); console.log("Second");Output:
First Second ThirdMore details
The browser environment includes several cooperating components:
- The JavaScript engine
- The call stack
- Browser APIs
- The microtask queue
- Task queues
- The event loop
- The rendering system
When a timer is created, the browser tracks it outside the JavaScript call stack. After the delay has elapsed, its callback becomes eligible to enter a task queue.
The event loop generally performs work in this order:
- Execute the current task.
- Empty the microtask queue.
- Give the browser an opportunity to render.
- Begin another task.
A zero-millisecond timeout does not run immediately. It specifies a minimum delay before the callback can be queued:
setTimeout(callback, 0);The callback must still wait for the current code and all queued microtasks to complete.
Long-running synchronous code blocks the event loop:
const start = Date.now(); while (Date.now() - start < 5000) { // Blocks the main thread }During this period, the page may not respond to input, process callbacks, or update its display.
Finished reviewing this question?043
What is the call stack?
Short answerThe call stack is the data structure JavaScript uses to track active function executions.
When a function is called, a stack frame is added. When the function returns or throws an error, its frame is removed.
Why it mattersJavaScript executes the function at the top of the stack before returning to the function below it.
function first() { second(); } function second() { console.log("Hello"); } first();More details
A simplified execution sequence is:
Add global execution context Add first() Add second() Add console.log() Remove console.log() Remove second() Remove first()Each stack frame stores information such as:
- Function arguments
- Local variables
- The current execution position
- The function’s
thisvalue - A reference to its lexical environment
Recursive functions add a new frame for every call:
function countDown(number) { if (number === 0) { return; } countDown(number - 1); } countDown(5);Recursion without a valid stopping condition eventually exceeds the available stack:
function recurse() { recurse(); } recurse(); // RangeError: Maximum call stack size exceededStack traces show the chain of active function calls at the time an error occurred, making them important for debugging.
Finished reviewing this question?044
What is the difference between synchronous and asynchronous code?
Short answerSynchronous code executes sequentially, with each operation completing before the next one begins.
Asynchronous code starts an operation and allows other JavaScript to continue while waiting. Its result is handled later through a callback, Promise, event, or
asyncfunction.Why it mattersAsynchronous programming prevents waiting operations such as network requests and timers from unnecessarily blocking other work.
console.log("Before"); setTimeout(() => { console.log("Timer finished"); }, 1000); console.log("After");More details
Synchronous code blocks further JavaScript execution until it finishes:
const result = performCalculation(); console.log(result);Asynchronous APIs return control before their work completes:
fetch("/api/users") .then(response => response.json()) .then(users => console.log(users)); console.log("Request started");async/awaitmakes asynchronous code look sequential, butawaitdoes not block the entire JavaScript thread:async function loadUsers() { const response = await fetch("/api/users"); const users = await response.json(); return users; }The async function pauses at
await, while other eligible work can continue.Asynchronous code is helpful for:
- Network requests
- Timers
- User interactions
- File operations in supported environments
- Background work
- Streams and animations
CPU-intensive JavaScript is still blocking unless it is divided into smaller tasks or moved to a worker.
Finished reviewing this question?045
What are microtasks and macrotasks?
Short answerMicrotasks and tasks—often informally called macrotasks—are scheduled work with different priorities.
After the current synchronous code finishes, JavaScript empties the microtask queue before starting the next task.
Why it mattersPromise callbacks are microtasks, while timer callbacks are tasks. Therefore, a resolved Promise usually runs before a zero-delay timer.
setTimeout(() => console.log("Timer"), 0); Promise.resolve().then(() => { console.log("Promise"); }); console.log("Synchronous");Output:
Synchronous Promise TimerMore details
Common microtask sources include:
- Promise reaction callbacks
- Code after
await queueMicrotask()MutationObservercallbacks
Common task sources include:
setTimeoutsetInterval- User-interface events
- Message events
- Some network and platform callbacks
Microtasks added while processing microtasks run before the browser moves to the next task:
queueMicrotask(() => { console.log("Microtask 1"); queueMicrotask(() => { console.log("Microtask 2"); }); });Excessive microtasks can delay rendering and other tasks. This is called microtask starvation:
function repeat() { queueMicrotask(repeat); } repeat();Browser scheduling details are defined in terms of tasks rather than “macrotasks,” but “macrotask” is commonly used in interviews to distinguish task queues from the microtask queue.
Finished reviewing this question?046
In what order do synchronous code, promises, and timers execute?
Short answerThe usual order is:
- Synchronous code
- Microtasks, including Promise callbacks and continuations after
await - The next task, such as a timer callback
Microtasks are emptied after the current task completes and before another task begins.
Why it mattersPromise callbacks therefore run before timer callbacks when both become eligible during the same task.
console.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D");Output:
A D C BMore details
Nested scheduling changes the order:
console.log("1"); setTimeout(() => { console.log("2"); Promise.resolve().then(() => { console.log("3"); }); }, 0); Promise.resolve().then(() => { console.log("4"); setTimeout(() => { console.log("5"); }, 0); }); console.log("6");Output:
1 6 4 2 3 5The reasoning is:
- Synchronous code prints
1and6. - The first Promise callback runs and prints
4. - The first timer task prints
2. - Its Promise callback runs as a microtask and prints
3. - The timer created inside the earlier Promise callback prints
5.
An
awaitcontinuation is also scheduled through the microtask mechanism:async function run() { console.log("Inside 1"); await Promise.resolve(); console.log("Inside 2"); } console.log("Outside 1"); run(); console.log("Outside 2");Output:
Outside 1 Inside 1 Outside 2 Inside 2Finished reviewing this question?047
What is a Promise?
Short answerA Promise is an object representing the eventual completion or failure of an asynchronous operation.
It provides a structured way to register success and failure handlers and to compose asynchronous operations without deeply nested callbacks.
Why it mattersthenhandles fulfillment,catchhandles rejection, andfinallyruns after settlement regardless of the outcome.fetch("/api/users") .then(response => response.json()) .then(users => console.log(users)) .catch(error => console.error(error)) .finally(() => console.log("Finished"));More details
A Promise can be created with an executor function:
const promise = new Promise((resolve, reject) => { const succeeded = true; if (succeeded) { resolve("Completed"); } else { reject(new Error("Failed")); } });The executor runs synchronously:
console.log("Before"); new Promise(resolve => { console.log("Executor"); resolve(); }); console.log("After");Output:
Before Executor AfterPromise handlers always run asynchronously as microtasks, even when the Promise is already settled:
Promise.resolve("Done").then(console.log); console.log("Synchronous");Output:
Synchronous Donethenreturns a new Promise, enabling chaining:Promise.resolve(2) .then(value => value * 2) .then(value => value + 1) .then(console.log); // 5Returning a Promise from a handler causes the next step to wait for it:
fetch("/api/user") .then(response => response.json()) .then(user => fetch(`/api/orders/${user.id}`)) .then(response => response.json());Throwing inside a handler rejects the Promise returned by that handler:
Promise.resolve() .then(() => { throw new Error("Failed"); }) .catch(error => console.error(error));Finished reviewing this question?048
What states can a Promise have?
Short answerA Promise has three possible states:
- Pending: The operation has not settled.
- Fulfilled: The operation completed successfully and produced a value.
- Rejected: The operation failed and produced a reason, usually an
Error.
A fulfilled or rejected Promise is described as settled.
Why it mattersA Promise can move from pending to fulfilled or rejected only once. Its final state and result cannot later be changed.
const promise = new Promise(resolve => { resolve("First"); resolve("Second"); // Ignored });More details
These transitions are possible:
pending → fulfilled pending → rejectedThese transitions are not possible:
fulfilled → rejected rejected → fulfilled settled → pendingResolving a Promise with another Promise makes the outer Promise adopt the other Promise’s eventual state:
const inner = new Promise(resolve => { setTimeout(() => resolve("Done"), 1000); }); const outer = new Promise(resolve => { resolve(inner); }); outer.then(console.log); // "Done"A Promise may be resolved but still pending while it follows another Promise. For everyday interviews, “resolved” is often treated as synonymous with “fulfilled,” but the specification distinguishes them.
A rejection should normally use an
Errorobject:reject(new Error("Unable to load user"));This preserves a useful message and stack trace.
An unhandled rejection occurs when a rejected Promise has no rejection handler. Browsers may report it in the console and emit an
unhandledrejectionevent.Finished reviewing this question?049
How do async and await work?
Short answerDeclaring a function with
asyncmakes it always return a Promise.awaitpauses that async function until a value or Promise settles. If it fulfills,awaitproduces its value. If it rejects,awaitthrows the rejection reason inside the async function.Why it mattersasync/awaitprovides syntax for writing Promise-based workflows in a readable, sequential style.async function loadUser() { const response = await fetch("/api/user"); const user = await response.json(); return user; }More details
Returning a normal value from an async function creates a fulfilled Promise:
async function getNumber() { return 42; } getNumber().then(console.log); // 42Throwing creates a rejected Promise:
async function fail() { throw new Error("Failed"); } fail().catch(console.error);awaitaccepts any value. A non-Promise value behaves like an already fulfilled Promise, but continuation still occurs asynchronously:async function run() { console.log("A"); await 42; console.log("C"); } run(); console.log("B");Output:
A B CErrors can be handled with
tryandcatch:async function loadUser() { try { const response = await fetch("/api/user"); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error(error); throw error; } }Independent operations should not be awaited one after another unnecessarily:
const [user, products] = await Promise.all([ fetchUser(), fetchProducts() ]);This starts both operations before waiting for them together.
Finished reviewing this question?050
How do Promise.all, Promise.allSettled, Promise.race, and Promise.any differ?
Short answerThese static methods combine multiple asynchronous values:
Promise.allfulfills when all inputs fulfill and rejects when the first input rejects.Promise.allSettledwaits for every input and always fulfills with their outcomes.Promise.racesettles with the first input to settle.Promise.anyfulfills with the first input to fulfill and rejects only if every input rejects.
Why it mattersChoose the method based on whether every result is required, partial failures are acceptable, or only the first outcome matters.
More details
Promise.allpreserves input order, regardless of completion order:const results = await Promise.all([ fetchUser(), fetchProducts(), fetchSettings() ]);If any input rejects, the combined Promise rejects immediately. Other operations are not automatically cancelled.
Promise.allSettledreturns status objects:const results = await Promise.allSettled([ Promise.resolve("A"), Promise.reject(new Error("Failed")) ]);Result shape:
[ { status: "fulfilled", value: "A" }, { status: "rejected", reason: Error("Failed") } ]Promise.racereacts to the first settlement, whether successful or failed:const result = await Promise.race([ fetchData(), timeoutPromise(5000) ]);Promise.anyignores rejections until one input fulfills:const fastestSuccess = await Promise.any([ fetchFromServerA(), fetchFromServerB() ]);If every input rejects,
Promise.anyrejects with anAggregateError.Empty iterables behave differently:
Promise.all([]); // Fulfilled with [] Promise.allSettled([]); // Fulfilled with [] Promise.any([]); // Rejected with AggregateError Promise.race([]); // Remains pendingFinished reviewing this question?051
How should errors be handled in asynchronous code?
Short answerPromise errors should be handled with
.catch()or withtry...catcharound awaited operations.Errors should normally be represented by
Errorobjects, handled at a level that can meaningfully recover, and rethrown when the caller still needs to know that the operation failed.Why it mattersA
try...catchblock can catch an awaited Promise rejection:try { const user = await fetchUser(); } catch (error) { console.error("Unable to load user", error); }More details
Promise chains propagate rejections to the nearest rejection handler:
fetchUser() .then(user => fetchOrders(user.id)) .then(orders => renderOrders(orders)) .catch(error => { showErrorMessage(error); });A
try...catchblock does not catch a Promise rejection unless the Promise is awaited:try { fetchUser(); // Rejection is not caught here } catch (error) { console.error(error); }Correct version:
try { await fetchUser(); } catch (error) { console.error(error); }Fetch only rejects for certain network-level failures. HTTP error statuses must be checked explicitly:
const response = await fetch("/api/user"); if (!response.ok) { throw new Error(`Request failed: ${response.status}`); }Avoid silently swallowing errors:
try { await saveUser(); } catch (error) { // Empty catch hides the failure }Add contextual information while preserving the original cause:
try { await saveUser(); } catch (error) { throw new Error("Could not save user", { cause: error }); }finallyis appropriate for cleanup:setLoading(true); try { await loadData(); } finally { setLoading(false); }Finished reviewing this question?052
How can an HTTP request be cancelled?
Short answerRequests made with the Fetch API can be cancelled using an
AbortController.Its
signalis passed tofetch, and callingabort()tells the operation to stop. The Fetch Promise then rejects, normally with an abort-related error.Why it mattersCancellation is useful when a component is removed, the user starts a newer request, or an operation exceeds a time limit.
const controller = new AbortController(); fetch("/api/users", { signal: controller.signal }); controller.abort();More details
Cancellation can be handled explicitly:
const controller = new AbortController(); try { const response = await fetch("/api/users", { signal: controller.signal }); const users = await response.json(); } catch (error) { if (error.name === "AbortError") { console.log("Request cancelled"); } else { throw error; } }A single signal can cancel multiple related operations:
const controller = new AbortController(); const requests = [ fetch("/api/user", { signal: controller.signal }), fetch("/api/products", { signal: controller.signal }) ]; controller.abort();Some environments support timeout signals:
const response = await fetch("/api/users", { signal: AbortSignal.timeout(5000) });When implementing search suggestions, cancel the previous request before starting a new one:
let currentController; async function search(query) { currentController?.abort(); currentController = new AbortController(); const response = await fetch( `/api/search?q=${encodeURIComponent(query)}`, { signal: currentController.signal } ); return response.json(); }Aborting Fetch does not guarantee that the server stops work already started. It primarily stops the client from continuing to wait for or process the response.
Finished reviewing this question?053
How can multiple asynchronous operations be executed in parallel?
Short answerStart all independent asynchronous operations before awaiting their combined result.
Promise.allis usually appropriate when every operation must succeed.Promise.allSettledis useful when all outcomes are needed even if some operations fail.Why it mattersSequential awaits unnecessarily delay independent operations:
const user = await fetchUser(); const products = await fetchProducts();Parallel version:
const [user, products] = await Promise.all([ fetchUser(), fetchProducts() ]);More details
Suppose each operation takes one second.
Sequential execution takes approximately two seconds:
const user = await fetchUser(); const products = await fetchProducts();The second operation does not begin until the first finishes.
Parallel execution takes approximately one second:
const userPromise = fetchUser(); const productsPromise = fetchProducts(); const [user, products] = await Promise.all([ userPromise, productsPromise ]);Only independent operations should run in parallel. Dependent operations must wait for the required value:
const user = await fetchUser(); const orders = await fetchOrders(user.id);When processing an array in parallel:
const users = await Promise.all( userIds.map(id => fetchUser(id)) );Avoid
forEachwith async callbacks:// Does not wait for completion userIds.forEach(async id => { await fetchUser(id); });Unrestricted parallelism can overload a browser, network, or server when the input is large. A concurrency limit may be necessary:
async function processInBatches(items, batchSize) { const results = []; for ( let index = 0; index < items.length; index += batchSize ) { const batch = items.slice(index, index + batchSize); const batchResults = await Promise.all( batch.map(item => processItem(item)) ); results.push(...batchResults); } return results; }Parallel execution improves total duration when operations are independent, but it does not make the individual operations themselves execute faster.
Finished reviewing this question?
05Browser and DOM18 questions
054
What is the DOM?
Short answerThe Document Object Model, or DOM, is the browser’s object-based representation of an HTML or XML document.
It represents the document as a tree of nodes and provides APIs that JavaScript can use to read, create, update, move, and remove content.
Why it mattersHTML elements become objects that JavaScript can interact with:
const heading = document.querySelector("h1"); heading.textContent = "Updated title";More details
The DOM tree contains several node types:
- Document nodes
- Element nodes
- Text nodes
- Comment nodes
- Document fragments
For this HTML:
<main> <h1>Hello</h1> </main>A simplified tree is:
document └── html └── body └── main └── h1 └── "Hello"The DOM is not part of the JavaScript language itself. It is a Web API supplied by browser environments.
JavaScript can use DOM APIs to:
- Find elements
- Change content and attributes
- Modify styles and classes
- Attach event listeners
- Create or remove elements
- Measure element positions and sizes
DOM updates may trigger browser rendering work, so large or repeated changes can affect performance.
Finished reviewing this question?055
What is the difference between the DOM and the HTML source?
Short answerThe HTML source is the text initially sent to the browser. The DOM is the live object tree the browser creates after parsing that source.
JavaScript, browser error correction, user interaction, and dynamic rendering can change the DOM without changing the original HTML response.
Why it mattersThe page source and current DOM may contain different content after JavaScript runs.
document.body.append("Added by JavaScript");This changes the DOM but not the server’s original HTML file.
More details
Browsers may correct invalid markup while building the DOM:
<table> <tr> <td>Item</td> </tr> </table>The browser may insert a
<tbody>element into the DOM even if it was not present in the source.The source is generally static for a particular response. The DOM is live and can change because of:
- JavaScript
- Form input
- Browser extensions
- Client-side frameworks
- DOM APIs
- Browser parsing rules
“View Page Source” normally shows the original response. Browser developer tools normally show the current DOM.
The DOM is also different from the visual rendering of the page. CSS can hide, reorder, or visually transform elements without changing their DOM structure.
Finished reviewing this question?056
How do you select, create, update, and remove DOM elements?
Short answerElements can be selected with methods such as
querySelectorandquerySelectorAll, created withcreateElement, updated through properties and attributes, and removed withremove.Why it mattersA common workflow is to create an element, configure it, and insert it into the document.
const item = document.createElement("li"); item.textContent = "New item"; document.querySelector("ul").append(item);More details
Common selection methods include:
document.querySelector(".card"); document.querySelectorAll(".card"); document.getElementById("profile");querySelectorreturns the first match ornull.querySelectorAllreturns a staticNodeList.Content and properties can be updated directly:
const image = document.querySelector("img"); image.src = "/avatar.png"; image.alt = "User avatar";Attributes can be managed explicitly:
element.setAttribute("aria-label", "Close"); element.getAttribute("aria-label"); element.removeAttribute("aria-label");Classes can be managed with
classList:element.classList.add("active"); element.classList.remove("hidden"); element.classList.toggle("selected");Elements can be inserted with:
parent.append(element); parent.prepend(element); reference.before(element); reference.after(element);They can be replaced or removed:
oldElement.replaceWith(newElement); element.remove();For many insertions, a
DocumentFragmentcan group work before insertion:const fragment = document.createDocumentFragment(); for (const name of names) { const item = document.createElement("li"); item.textContent = name; fragment.append(item); } list.append(fragment);Finished reviewing this question?057
What is event bubbling?
Short answerEvent bubbling is the phase in which an event moves from its target element upward through its ancestors.
Most common DOM events bubble, allowing parent elements to respond to events that began on their descendants.
Why it mattersClicking the button can trigger listeners on both the button and its parent:
parent.addEventListener("click", () => { console.log("Parent"); }); button.addEventListener("click", () => { console.log("Button"); });The button listener normally runs before the parent listener.
More details
An event generally moves through three phases:
- Capturing: from the document toward the target
- Target: at the element where the event occurred
- Bubbling: from the target back toward the document
Bubbling makes event delegation possible:
list.addEventListener("click", event => { const item = event.target.closest("li"); if (item) { console.log(item.dataset.id); } });Not every event bubbles. For example,
focusandblurdo not normally bubble, althoughfocusinandfocusoutdo.Bubbling can be stopped:
event.stopPropagation();However, stopping propagation can interfere with unrelated parent behavior and should be used intentionally.
Finished reviewing this question?058
What is event capturing?
Short answerEvent capturing is the phase in which an event travels from the top of the document tree down toward its target.
Event listeners use the bubbling phase by default. A listener can participate in capturing by setting the
captureoption totrue.Why it mattersA capturing parent listener runs before a target’s ordinary bubbling listener.
parent.addEventListener( "click", () => console.log("Parent capture"), { capture: true } );More details
The event order is generally:
document capture → ancestor capture → target → ancestor bubble → document bubbleThe shorter boolean form is also supported:
parent.addEventListener("click", handler, true);The options object is clearer and can include other settings:
element.addEventListener("click", handler, { capture: true, once: true, passive: true });Capture listeners can be useful when:
- An event does not bubble normally
- A parent must observe an event before a child handles it
- Centralized interception is required
A listener must normally be removed with the same capture setting used when it was added:
element.removeEventListener("click", handler, { capture: true });Finished reviewing this question?059
What is event delegation?
Short answerEvent delegation attaches one listener to a shared ancestor instead of attaching separate listeners to every descendant.
The ancestor uses event bubbling and information such as
event.targetto determine which descendant initiated the event.Why it mattersDelegation reduces the number of listeners and automatically supports matching elements added later.
list.addEventListener("click", event => { const button = event.target.closest(".delete-button"); if (!button || !list.contains(button)) { return; } deleteItem(button.dataset.id); });More details
Without delegation, every button needs a listener:
document .querySelectorAll(".delete-button") .forEach(button => { button.addEventListener("click", handleDelete); });Newly added buttons would require additional listeners.
With delegation, the ancestor’s listener handles current and future descendants:
list.insertAdjacentHTML( "beforeend", '<button class="delete-button">Delete</button>' );closest()is usually safer than checking onlyevent.target.matches()because the click may originate from a nested icon or text element.Confirm that the matched element belongs to the intended container:
if (!button || !list.contains(button)) { return; }Delegation works best for bubbling events. For non-bubbling behavior, use a related bubbling event or a capture listener where appropriate.
Finished reviewing this question?060
What is the difference between event.target and event.currentTarget?
Short answerevent.targetis the element where the event originated.event.currentTargetis the element whose event listener is currently running.Why it mattersDuring event delegation, the target may be a descendant while the current target is the ancestor holding the listener.
list.addEventListener("click", event => { console.log(event.target); console.log(event.currentTarget); // list });More details
Given this markup:
<button id="save"> <span>Save</span> </button>And this listener:
const button = document.querySelector("#save"); button.addEventListener("click", event => { console.log(event.target); console.log(event.currentTarget); });Clicking the
<span>produces:event.target: the<span>event.currentTarget: the<button>
targetnormally stays the same as the event propagates.currentTargetchanges for each listener.Outside the listener callback,
currentTargetis typicallynull:button.addEventListener("click", event => { setTimeout(() => { console.log(event.currentTarget); // null }); });Save the value during the callback if it is needed later.
Finished reviewing this question?061
What do preventDefault and stopPropagation do?
Short answerpreventDefault()prevents the browser’s default action for an event.stopPropagation()prevents the event from continuing through the remaining ancestors during capture or bubbling.They affect different parts of event handling.
Why it mattersPreventing a form submission does not automatically stop the event from bubbling.
form.addEventListener("submit", event => { event.preventDefault(); });More details
Common default browser actions include:
- Following a link
- Submitting a form
- Checking a checkbox
- Opening a context menu
- Performing some drag-and-drop actions
A default action can be prevented only when the event is cancelable:
if (event.cancelable) { event.preventDefault(); }stopPropagation()stops movement to other elements but does not stop other listeners on the same element.To stop those as well, use:
event.stopImmediatePropagation();Passive listeners cannot call
preventDefault():element.addEventListener("touchmove", handler, { passive: true });Passive listeners help scrolling performance by promising that the handler will not cancel the event.
Both methods should be used carefully because preventing expected browser behavior or propagation may break accessibility and other components.
Finished reviewing this question?062
What is the difference between DOMContentLoaded and load?
Short answerDOMContentLoadedfires after the initial HTML has been parsed and the DOM has been constructed.loadfires later, after the document and dependent resources such as images, stylesheets, and frames have finished loading.Why it mattersUse
DOMContentLoadedwhen code only needs the DOM. Useloadwhen it depends on all page resources being available.document.addEventListener("DOMContentLoaded", () => { console.log("DOM ready"); }); window.addEventListener("load", () => { console.log("Page resources loaded"); });More details
Deferred scripts execute after parsing and before
DOMContentLoaded:<script defer src="app.js"></script>Module scripts are deferred by default:
<script type="module" src="app.js"></script>A normal script placed at the end of
<body>can often access earlier DOM elements without waiting forDOMContentLoaded.The
loadevent also exists on individual resources:image.addEventListener("load", () => { console.log("Image loaded"); });If code may run after
DOMContentLoadedhas already fired, check the document state:function initialize() { // Set up application } if (document.readyState === "loading") { document.addEventListener( "DOMContentLoaded", initialize, { once: true } ); } else { initialize(); }Finished reviewing this question?063
What is the difference between innerHTML, innerText, and textContent?
Short answerinnerHTMLreads or writes HTML markup.textContentreads or writes the textual content of a node and its descendants.innerTextrepresents rendered text and takes visibility and layout into account.
Why it mattersUse
textContentfor plain text. UseinnerHTMLonly when HTML parsing is required and the content is trusted or properly sanitized.element.textContent = "<strong>Hello</strong>"; // Displays the markup as text element.innerHTML = "<strong>Hello</strong>"; // Creates a strong elementMore details
innerHTMLparses assigned strings as markup:container.innerHTML = ` <button class="save">Save</button> `;Assigning untrusted input to it can create an XSS vulnerability:
container.innerHTML = userProvidedContent;textContentdoes not parse markup, making it appropriate for user-provided plain text:container.textContent = userProvidedContent;textContentincludes text from hidden elements and returns content close to the underlying DOM structure.innerTextreflects rendered text:<div id="message"> Visible <span hidden>Hidden</span> </div>textContentincludes"Hidden", whileinnerTextgenerally does not.Reading
innerTextmay require layout calculation, making it potentially more expensive thantextContent.Setting
textContentorinnerHTMLreplaces existing child nodes and removes their directly attached event listeners.Finished reviewing this question?064
What are custom events?
Short answerCustom events are application-defined DOM events used to communicate that a meaningful action or state change has occurred.
They are created with
CustomEventand can carry additional information through thedetailproperty.Why it mattersCustom events let components communicate without calling each other’s internal functions directly.
const event = new CustomEvent("cart:item-added", { detail: { productId: 42 } }); element.dispatchEvent(event);More details
A listener receives the custom data through
event.detail:element.addEventListener("cart:item-added", event => { console.log(event.detail.productId); });Custom events do not bubble by default. Enable bubbling when delegation or ancestor communication is required:
const event = new CustomEvent("cart:item-added", { bubbles: true, detail: { productId: 42 } });They can also be cancelable:
const event = new CustomEvent("dialog:before-close", { cancelable: true }); const allowed = dialog.dispatchEvent(event); if (allowed) { dialog.close(); }A listener can prevent the action:
dialog.addEventListener("dialog:before-close", event => { if (hasUnsavedChanges) { event.preventDefault(); } });Custom event names should be descriptive and namespaced to reduce collisions.
Finished reviewing this question?065
What are the differences between cookies, localStorage, and sessionStorage?
Short answerAll three store data in the browser, but they differ in lifetime, capacity, server interaction, and scope.
- Cookies can be sent automatically with HTTP requests and support security attributes.
localStoragepersists until explicitly cleared.sessionStoragelasts for the lifetime of a particular browser tab.
Why it mattersWeb Storage is convenient for non-sensitive client-side data. Cookies are appropriate when the server must receive the value, particularly for session management.
More details
A simplified comparison:
Feature Cookies localStoragesessionStorageLifetime Configurable Until cleared Current tab session Sent to server Usually, when applicable No No API String-based cookie header Key/value Key/value Typical capacity Small Larger Larger Server can set it Yes No No Web Storage stores strings:
localStorage.setItem( "preferences", JSON.stringify({ theme: "dark" }) ); const preferences = JSON.parse( localStorage.getItem("preferences") );Storage operations are synchronous and can block the main thread when overused.
Sensitive data should not be placed in Web Storage because JavaScript running on the origin can access it. An XSS vulnerability could expose it.
Authentication cookies can use protections such as:
HttpOnlySecureSameSite- Restricted
DomainandPath - Appropriate expiration
HttpOnlycookies cannot be read through JavaScript, reducing token theft through XSS, although XSS remains dangerous.Finished reviewing this question?066
What is the same-origin policy?
Short answerThe same-origin policy is a browser security rule that restricts how documents or scripts from one origin can access resources and data from another origin.
An origin is defined by its scheme, host, and port.
Why it mattersThese URLs do not all share the same origin:
https://example.com http://example.com https://api.example.com https://example.com:8443Each differs by scheme, host, or port.
More details
The policy restricts operations such as:
- Reading cross-origin responses
- Accessing another origin’s DOM
- Reading another origin’s storage
- Inspecting cross-origin frames
Some cross-origin operations are allowed in limited ways. For example, a page can often display a cross-origin image but cannot freely inspect its pixel data through a canvas.
Cross-origin network requests may be permitted through CORS. Cross-document communication can use
window.postMessage():otherWindow.postMessage( { type: "READY" }, "https://trusted.example" );The receiving page should validate the sender:
window.addEventListener("message", event => { if (event.origin !== "https://trusted.example") { return; } console.log(event.data); });The same-origin policy is enforced by browsers. It does not prevent direct server-to-server requests.
Finished reviewing this question?067
What is CORS?
Short answerCross-Origin Resource Sharing, or CORS, is an HTTP-header mechanism that allows a server to tell browsers which origins may read its responses.
CORS relaxes selected same-origin policy restrictions. It is configured by the server and enforced by the browser.
Why it mattersThe server may allow a particular origin with a response header:
Access-Control-Allow-Origin: https://app.example.comThe frontend cannot grant itself CORS permission.
More details
Some cross-origin requests are considered simple and may be sent directly. The browser still checks the response headers before exposing the response to JavaScript.
Other requests trigger a preflight request using
OPTIONS:OPTIONS /api/users Origin: https://app.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-TypeThe server responds with allowed methods, headers, and origins.
Credentialed requests require explicit configuration:
fetch("https://api.example.com/user", { credentials: "include" });The server must allow credentials and cannot use
*as the allowed origin for that credentialed response.CORS is not an authentication system. A server must still validate permissions and credentials.
CORS also does not stop non-browser clients from making requests. Its main purpose is controlling whether browser JavaScript can access cross-origin responses.
Finished reviewing this question?068
How does the Fetch API work?
Short answerThe Fetch API performs HTTP requests and returns a Promise that fulfills with a
Responseobject.The response body is read asynchronously using methods such as
json,text,blob, orarrayBuffer.Why it mattersFetch does not automatically reject for HTTP error statuses such as
404or500. The response status must be checked.const response = await fetch("/api/users"); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const users = await response.json();More details
A POST request can be made with:
const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Alex" }) });Useful response properties include:
response.ok; response.status; response.statusText; response.headers; response.url;The body is a stream and normally can be consumed only once:
await response.json(); await response.text(); // Error: body already consumedClone the response first if two consumers are genuinely required:
const copy = response.clone();Fetch can include an abort signal:
const controller = new AbortController(); fetch("/api/users", { signal: controller.signal });Request options also control credentials, caching, redirects, and request mode. Application code should handle:
- Network failures
- HTTP failures
- Invalid response data
- Cancellation
- Timeouts
- Loading and retry states
Finished reviewing this question?069
What is the difference between Fetch and XMLHttpRequest?
Short answerFetch is the modern Promise-based API for HTTP requests.
XMLHttpRequest, or XHR, is an older event- and callback-based API.Fetch usually provides cleaner composition, better integration with
async/await, and direct access to request and response streams.Why it mattersFetch is preferred for most modern application code, while XHR may still appear in older systems or specialized upload-progress implementations.
More details
A Fetch request:
const response = await fetch("/api/users"); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const users = await response.json();A comparable XHR request:
const request = new XMLHttpRequest(); request.open("GET", "/api/users"); request.onload = () => { if (request.status >= 200 && request.status < 300) { const users = JSON.parse(request.responseText); console.log(users); } }; request.onerror = () => { console.error("Network error"); }; request.send();Key differences include:
- Fetch uses Promises.
- XHR uses events and callbacks.
- Fetch supports modern stream APIs.
- XHR has established upload-progress events.
- Fetch requires explicit checking of HTTP failure statuses.
- Cancellation uses
AbortControllerwith Fetch andabort()with XHR.
Download progress can be implemented with Fetch streams, although it requires manual processing. Upload progress support has historically been simpler with XHR.
Finished reviewing this question?070
What is the History API?
Short answerThe History API lets browser applications interact with the current tab’s session history.
It can add or replace history entries and respond when the user navigates backward or forward.
Why it mattersSingle-page applications use it to update the URL without performing a full page reload.
history.pushState( { page: "profile" }, "", "/profile" );More details
The main methods are:
history.pushState(state, "", url); history.replaceState(state, "", url); history.back(); history.forward(); history.go(-2);pushStateadds a new entry.replaceStateupdates the current entry without adding another one.Back and forward navigation can be observed with
popstate:window.addEventListener("popstate", event => { renderRoute(location.pathname, event.state); });Calling
pushStateorreplaceStatedoes not itself triggerpopstate.The new URL must generally have the same origin as the current page.
Client-side routing also requires server configuration. If a user directly loads
/profile, the server must return the application rather than an unrelated404response.The History API should be paired with semantic links where possible so navigation remains accessible and behaves correctly with normal browser features.
Finished reviewing this question?071
What are Web Workers, and when should they be used?
Short answerWeb Workers run JavaScript in a background thread separate from the page’s main JavaScript thread.
They are useful for CPU-intensive work that would otherwise block rendering and user interaction.
Why it mattersWorkers cannot directly access the DOM. They communicate with the main thread by exchanging messages.
const worker = new Worker("./worker.js"); worker.postMessage({ values: [1, 2, 3] }); worker.addEventListener("message", event => { console.log(event.data); });More details
Worker code receives and sends messages:
// worker.js self.addEventListener("message", event => { const result = performExpensiveCalculation( event.data.values ); self.postMessage(result); });Suitable use cases include:
- Processing large datasets
- Image or audio manipulation
- Complex calculations
- Parsing large files
- Compression
- Cryptographic work
- Background data transformation
Workers are usually unnecessary for ordinary network requests because Fetch is already asynchronous.
Data is normally transferred using the structured clone algorithm, so sending very large values has a cost:
worker.postMessage(largeObject);Transferable objects can transfer ownership of certain data without copying:
worker.postMessage(buffer, [buffer]);After transfer, the original thread can no longer use that buffer.
Common worker types include:
- Dedicated Workers, used by one page context
- Shared Workers, potentially shared by multiple contexts
- Service Workers, used for network interception, caching, and offline capabilities
These worker types solve different problems and have different lifecycles and APIs.
Finished reviewing this question?
06Performance and optimization11 questions
072
What are debounce and throttle?
Short answerDebounce and throttle limit how frequently a function executes.
- Debounce waits until calls have stopped for a specified period before executing.
- Throttle allows execution at most once during each specified interval.
Why it mattersDebounce is useful when only the final action matters, such as search input. Throttle is useful when continuous updates are needed at a controlled rate, such as scroll tracking.
More details
A debounced search waits until the user pauses typing:
const search = debounce(query => { fetchResults(query); }, 300); input.addEventListener("input", event => { search(event.target.value); });A throttled scroll handler runs periodically while scrolling continues:
const handleScroll = throttle(() => { updateScrollPosition(window.scrollY); }, 200); window.addEventListener("scroll", handleScroll);A basic debounce implementation:
function debounce(callback, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { callback.apply(this, args); }, delay); }; }Debounce and throttle implementations may support:
- Leading-edge execution
- Trailing-edge execution
- Cancellation
- Immediate flushing
- A maximum waiting time
For visual updates tied to the browser’s refresh cycle,
requestAnimationFramemay be more appropriate than time-based throttling.Finished reviewing this question?073
What causes memory leaks in frontend applications?
Short answerA memory leak occurs when an application keeps references to data that it no longer needs, preventing garbage collection.
Common causes include forgotten event listeners, active timers, retained DOM nodes, long-lived closures, unbounded caches, subscriptions, and detached components.
Why it mattersMemory leaks gradually increase memory usage and may make an application slow or unstable during long sessions.
More details
A listener can keep its related data reachable:
function mount() { const largeData = loadLargeData(); function handleResize() { useData(largeData); } window.addEventListener("resize", handleResize); }If the component is removed but the listener remains,
handleResizeandlargeDatamay stay in memory.Cleanup should remove the listener:
window.removeEventListener("resize", handleResize);An
AbortControllercan manage listener cleanup:const controller = new AbortController(); window.addEventListener("resize", handleResize, { signal: controller.signal }); controller.abort();Other common cleanup operations include:
clearInterval(intervalId); observer.disconnect(); subscription.unsubscribe(); controller.abort();Detached DOM nodes can remain in memory if JavaScript still references them:
const removedElement = document.querySelector(".panel"); removedElement.remove(); // removedElement still references the detached nodeBrowser memory tools, heap snapshots, and allocation timelines help identify objects that remain reachable unexpectedly.
Finished reviewing this question?074
How does JavaScript garbage collection work?
Short answerJavaScript automatically reclaims memory occupied by values that are no longer reachable from active roots.
Modern engines primarily use reachability-based garbage collection, commonly involving tracing algorithms such as mark-and-sweep along with generational and incremental optimizations.
Why it mattersDevelopers do not manually free JavaScript objects. They help garbage collection by removing unnecessary references and cleaning up external resources.
More details
Garbage-collection roots typically include:
- The global object
- Active call-stack variables
- Active closures
- Registered callbacks
- Reachable DOM and platform objects
During a simplified mark-and-sweep process, the engine:
- Starts from known roots.
- Marks every value reachable from those roots.
- Reclaims unmarked values.
Circular references are not automatically leaks:
let first = {}; let second = {}; first.other = second; second.other = first; first = null; second = null;The objects can be collected because neither remains reachable from a root.
Setting a local variable to
nullis not usually necessary. It can help when a long-lived object holds a large value that is no longer needed:cache.largeResult = null;Garbage collection is nondeterministic. Code cannot rely on it happening at a particular moment.
Resources such as event listeners, network requests, observers, and timers often require explicit cleanup even though their associated JavaScript memory is managed automatically.
Finished reviewing this question?075
What are reflow and repaint?
Short answerReflow, often called layout, recalculates the size and position of elements.
Repaint redraws pixels when visual properties change without necessarily changing layout. Compositing combines rendered layers into the final image.
Why it mattersLayout changes are usually more expensive because they can affect multiple elements and may be followed by paint and compositing work.
More details
Properties that can trigger layout include:
- Width and height
- Margin and padding
- Font size
- Positioning values
- Content changes
- Adding or removing elements
Paint-only changes can include properties such as colors, backgrounds, borders, and shadows.
Properties such as
transformandopacitycan often be handled mainly during compositing, making them preferable for animations:.panel { transform: translateX(100px); opacity: 0.5; }Repeatedly mixing DOM reads and writes can cause layout thrashing:
for (const element of elements) { const width = element.offsetWidth; element.style.width = `${width + 10}px`; }Group reads and writes instead:
const widths = elements.map( element => element.offsetWidth ); elements.forEach((element, index) => { element.style.width = `${widths[index] + 10}px`; });Not every style change forces immediate work. Browsers often batch rendering until JavaScript yields, but reading layout-dependent measurements may force pending calculations.
Finished reviewing this question?076
What is the difference between async and defer on script elements?
Short answerBoth
asyncanddeferlet external scripts download without blocking HTML parsing.asyncexecutes each script as soon as it finishes downloading, without preserving document order.deferwaits until HTML parsing finishes and preserves script order.
Why it mattersUse
deferfor application scripts that depend on the DOM or each other. Useasyncfor independent scripts whose execution order does not matter.More details
A normal external script blocks parsing:
<script src="app.js"></script>The browser pauses HTML parsing while it downloads and executes the script.
Deferred scripts preserve their document order:
<script defer src="library.js"></script> <script defer src="app.js"></script>library.jsexecutes beforeapp.js, even ifapp.jsdownloads first. Deferred scripts execute beforeDOMContentLoaded.Async scripts execute as soon as they are ready:
<script async src="analytics.js"></script>Multiple async scripts may execute in any order and may run before or after parsing completes.
Module scripts are deferred by default:
<script type="module" src="app.js"></script>The
asyncattribute can also be added to a module script when immediate execution after dependency loading is appropriate.asyncanddeferapply to external classic scripts. Their behavior does not meaningfully apply to an ordinary inline classic script without asrc.Finished reviewing this question?077
What is lazy loading?
Short answerLazy loading delays downloading, initializing, or rendering a resource until it is likely to be needed.
It reduces initial page work and can improve startup time, bandwidth usage, and responsiveness.
Why it mattersImages and iframes can use native lazy loading:
<img src="/photo.jpg" alt="Mountain landscape" loading="lazy" >More details
Common lazy-loaded resources include:
- Images
- Iframes
- JavaScript modules
- Routes
- Large components
- Videos
- Data requested for off-screen content
JavaScript can detect when content approaches the viewport with
IntersectionObserver:const observer = new IntersectionObserver(entries => { for (const entry of entries) { if (!entry.isIntersecting) { continue; } loadContent(entry.target); observer.unobserve(entry.target); } }); observer.observe(document.querySelector(".gallery"));Modules can be loaded dynamically:
const module = await import("./editor.js"); module.initializeEditor();Lazy loading has tradeoffs:
- Loading too late can create visible delays.
- Layout space should be reserved to prevent shifts.
- Critical above-the-fold content should not normally be lazy-loaded.
- Errors and loading states must be handled.
- Excessively small chunks may add request overhead.
Prefetching can complement lazy loading by downloading likely future resources during idle time.
Finished reviewing this question?078
What is code splitting?
Short answerCode splitting divides a JavaScript application into smaller bundles that can be loaded independently.
It reduces the amount of JavaScript required for the initial page and delays less important code until it is needed.
Why it mattersDynamic
import()creates a natural split point in modern bundlers.const editorModule = await import("./editor.js"); editorModule.openEditor();More details
Common splitting strategies include:
- Route-based splitting
- Component-based splitting
- Feature-based splitting
- Splitting large third-party libraries
- Loading administrative tools only for authorized users
For example, an application does not need to load checkout code on its home page:
if (route === "/checkout") { const checkout = await import("./checkout.js"); checkout.render(); }Code splitting can improve:
- Initial bundle size
- Parse and compilation time
- Time to interactivity
- Cache efficiency
It can also create costs:
- Additional network requests
- Loading states
- Chunk-load failures
- Dependency duplication
- Delays when a feature is first opened
Preloading or prefetching important future chunks can reduce interaction delays. Bundle analysis tools help identify large dependencies and ineffective split points.
Finished reviewing this question?079
What is tree shaking?
Short answerTree shaking is a build optimization that removes exported code determined to be unused.
It relies primarily on the static structure of ES modules, allowing build tools to analyze
importandexportstatements without executing the code.Why it mattersIf a module exports several functions but an application imports only one, a bundler may exclude the unused exports from the production bundle.
// utilities.js export function add(a, b) { return a + b; } export function unusedFeature() { // ... }import { add } from "./utilities.js";More details
Static ES module syntax is easier to analyze than dynamic module patterns:
import { add } from "./math.js";Tree shaking is most effective when:
- ES modules are used
- Production optimizations are enabled
- Modules avoid unnecessary top-level side effects
- Packages correctly describe side-effect behavior
- Imports target tree-shakable entry points
A top-level side effect may require a module to remain:
registerGlobalPlugin();Package metadata can identify files that must be preserved for their side effects:
{ "sideEffects": [ "./src/global.css", "./src/register.js" ] }Tree shaking is different from minification:
- Tree shaking removes unused code.
- Minification rewrites retained code into a smaller form.
Importing an entire library through a non-tree-shakable entry point may prevent unused features from being removed.
Finished reviewing this question?080
What is memoization?
Short answerMemoization caches a function’s result based on its inputs and returns the cached result when the same inputs occur again.
It exchanges additional memory and cache-management complexity for reduced computation time.
Why it mattersMemoization is most suitable for deterministic, expensive functions that receive repeated inputs.
function memoize(callback) { const cache = new Map(); return argument => { if (cache.has(argument)) { return cache.get(argument); } const result = callback(argument); cache.set(argument, result); return result; }; }More details
A memoized calculation:
const calculate = memoize(number => { console.log("Calculating"); return number * number; }); calculate(5); // Calculates and returns 25 calculate(5); // Returns cached 25Multiple arguments require a reliable cache-key strategy:
function memoizeTwoArguments(callback) { const cache = new Map(); return (first, second) => { const key = `${first}:${second}`; if (!cache.has(key)) { cache.set(key, callback(first, second)); } return cache.get(key); }; }String-based keys can collide for complex inputs. Nested
Mapobjects or identity-based caches may be safer.Object arguments are compared by reference:
cache.get({ id: 1 }); // Different object each timeMemoization is less useful when:
- Inputs rarely repeat
- Computation is inexpensive
- Results change despite identical inputs
- The cache can grow indefinitely
- Cached data becomes stale
Frontend frameworks may provide memoization tools, but memoization itself has a cost. It should address a measured performance problem rather than be added automatically.
Finished reviewing this question?081
How can frontend JavaScript performance be measured and improved?
Short answerFrontend performance should be measured with browser tools and real-user data before optimization.
Improvements usually target loading, JavaScript execution, rendering, memory usage, network activity, and responsiveness.
Why it mattersA useful process is:
- Measure the current experience.
- Identify the largest bottleneck.
- Apply a focused change.
- Measure again.
- Confirm that user-facing performance improved.
More details
Useful browser tools and APIs include:
- Performance panel
- Network panel
- Memory profiler
- Lighthouse
- Performance API
PerformanceObserver- Coverage and bundle-analysis tools
Custom timings can be recorded:
performance.mark("render-start"); renderDashboard(); performance.mark("render-end"); performance.measure( "dashboard-render", "render-start", "render-end" );Common improvements include:
- Reducing initial JavaScript
- Code splitting and lazy loading
- Removing unused dependencies
- Optimizing images and fonts
- Caching static resources
- Avoiding layout thrashing
- Virtualizing long lists
- Moving heavy work to Web Workers
- Debouncing or throttling frequent handlers
- Reducing unnecessary renders
- Breaking up long tasks
Important user-facing metrics include:
- Largest Contentful Paint
- Interaction to Next Paint
- Cumulative Layout Shift
- Time to First Byte
Laboratory tests provide controlled comparisons, while real-user monitoring captures actual devices, networks, locations, and user behavior. Both are valuable.
Optimizing a fast operation that does not affect the user is usually less valuable than fixing a measured rendering, loading, or interaction bottleneck.
Finished reviewing this question?082
What is the difference between requestAnimationFrame and setTimeout?
Short answerrequestAnimationFrameschedules a callback before the browser’s next repaint.setTimeoutschedules a task after a minimum time delay, without synchronizing it with rendering.Why it mattersUse
requestAnimationFramefor visual updates and JavaScript-driven animations. UsesetTimeoutfor general delayed work.requestAnimationFrame(timestamp => { updateAnimation(timestamp); });More details
A
requestAnimationFrameloop can animate based on elapsed time:let previousTime; function animate(currentTime) { if (previousTime !== undefined) { const elapsed = currentTime - previousTime; updatePosition(elapsed); } previousTime = currentTime; requestAnimationFrame(animate); } requestAnimationFrame(animate);Using timestamps makes movement independent of the display’s refresh rate.
The browser can coordinate animation-frame callbacks with layout, paint, and compositing. It may pause or reduce them in background tabs to save resources.
setTimeoutprovides only a minimum delay:setTimeout(callback, 16);The callback may execute later if the call stack or task queue is busy. Repeated 16-millisecond timers are not reliably synchronized with the display.
Animation frames can be cancelled:
const frameId = requestAnimationFrame(animate); cancelAnimationFrame(frameId);Timers use separate cancellation:
const timeoutId = setTimeout(callback, 1000); clearTimeout(timeoutId);requestAnimationFrameis not automatically appropriate for expensive nonvisual calculations. Long callbacks still block the main thread and can cause dropped frames.Finished reviewing this question?
07Errors, security, and code quality10 questions
083
How do try, catch, finally, and throw work?
Short answerThese statements provide structured error handling:
trycontains code that may throw an error.catchhandles an error thrown inside the associatedtry.finallyruns aftertryandcatch, whether an error occurred or not.throwcreates or propagates an exception.
Why it mattersUse
try...catchwhen code can fail in a way the application can meaningfully handle.try { const data = JSON.parse(input); console.log(data); } catch (error) { console.error("Invalid JSON", error); } finally { console.log("Parsing attempt finished"); }More details
Any JavaScript value can technically be thrown, but an
Errorobject should normally be used:throw new Error("Unable to load user");Errorobjects provide useful information such as:namemessagestackcause
Custom validation can throw an error:
function setAge(age) { if (!Number.isInteger(age) || age < 0) { throw new RangeError( "Age must be a non-negative integer" ); } return age; }A caught error can be handled, transformed, or rethrown:
try { await saveUser(user); } catch (error) { throw new Error("Could not save user", { cause: error }); }finallyis useful for cleanup:setLoading(true); try { await loadData(); } catch (error) { showError(error); } finally { setLoading(false); }A
returninsidefinallyoverrides an earlier return or thrown error, so it should generally be avoided:function example() { try { throw new Error("Failed"); } finally { return "Success"; // Suppresses the error } }try...catchhandles synchronous exceptions and awaited Promise rejections. It does not catch an unawaited Promise rejection:try { fetchData(); // A later rejection is not caught here } catch (error) { // Does not handle the Promise rejection }Correct version:
try { await fetchData(); } catch (error) { console.error(error); }Finished reviewing this question?084
What is the difference between syntax, runtime, and logical errors?
Short answer- A syntax error means the code does not follow JavaScript’s grammar and cannot be parsed correctly.
- A runtime error occurs while otherwise valid code is executing.
- A logical error allows the program to run but produces incorrect behavior or results.
Why it mattersSyntax and runtime errors usually produce exceptions. Logical errors may not produce any error message, making tests and debugging particularly important.
More details
A syntax error prevents the affected script from being parsed:
function greet( { console.log("Hello"); }A runtime error happens during execution:
const user = null; console.log(user.name); // TypeErrorOther common runtime errors include:
ReferenceErrorTypeErrorRangeErrorURIError- Custom application errors
A logical error produces the wrong result:
function calculateDiscount(price) { return price * 1.2; // Increases the price }The code is valid and executes successfully, but the business logic is incorrect.
Tools for finding these errors include:
- Syntax highlighting and linters
- Browser developer tools
- Breakpoints
- Stack traces
- Logging
- Static type checking
- Automated tests
- Code review
Tests are especially valuable for logical errors because the JavaScript engine cannot know the intended business behavior.
Finished reviewing this question?085
What is cross-site scripting (XSS), and how can it be prevented?
Short answerCross-site scripting is a vulnerability in which untrusted content is interpreted and executed as code in another user’s browser within a trusted application’s context.
Prevention requires context-appropriate output encoding, safe DOM APIs, HTML sanitization when HTML is intentionally accepted, framework protections, and defense-in-depth controls.
Why it mattersUntrusted text should be inserted with a safe text API instead of being parsed as HTML:
message.textContent = userInput;Avoid inserting untrusted content directly with
innerHTML.More details
Common categories include:
- Stored XSS, where malicious content is persisted
- Reflected XSS, where malicious input is included in an immediate response
- DOM-based XSS, where unsafe client-side code writes attacker-controlled data into a dangerous DOM location
Unsafe example:
output.innerHTML = location.hash.slice(1);Safer plain-text version:
output.textContent = location.hash.slice(1);Security depends on where the value is inserted. HTML content, attributes, URLs, CSS, and JavaScript have different parsing rules and require context-specific protections.
Important protections include:
- Use framework output escaping correctly.
- Use safe sinks such as
textContent. - Sanitize untrusted HTML with a well-maintained sanitizer when HTML must be supported.
- Validate URL schemes and destinations.
- Avoid dangerous APIs such as
document.write. - Avoid constructing executable JavaScript from strings.
- Use Content Security Policy as an additional layer.
- Consider Trusted Types where supported.
Sanitization is different from validation. Validation checks whether input matches expected rules. Sanitization transforms allowed HTML to remove unsafe content.
Content Security Policy can reduce impact but should not be treated as the only XSS defense. Cookie attributes may also reduce certain consequences but do not prevent malicious script execution.
Finished reviewing this question?086
What is cross-site request forgery (CSRF), and how can it be prevented?
Short answerCross-site request forgery is an attack that tricks an authenticated browser into sending an unwanted state-changing request to a trusted application.
It commonly works when the browser automatically includes authentication credentials, such as session cookies, but the server does not verify that the intended application initiated the request.
Why it mattersCSRF prevention is primarily enforced on the server through protections such as unpredictable CSRF tokens, appropriate
SameSitecookies, and origin validation.More details
A malicious page might attempt to trigger an authenticated action:
<form action="https://bank.example/transfer" method="POST" > <input name="amount" value="1000"> <input name="destination" value="attacker"> </form>If the browser automatically sends the victim’s session cookie and the server accepts the request without additional verification, the action may succeed.
Common protections include:
- Synchronizer CSRF tokens
- Properly implemented signed double-submit cookies
SameSitecookie attributes- Verification of
OriginorReferer - Reauthentication for highly sensitive actions
- Custom request headers where appropriate
- Avoiding state changes through safe methods such as
GET
A token-based request may look like:
await fetch("/api/profile", { method: "PATCH", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, credentials: "same-origin", body: JSON.stringify(profile) });The server must verify that the token is valid, secret, unpredictable, and associated with the correct session.
SameSiteis valuable, but it should generally be treated as defense in depth unless the application meets strict conditions allowing it to stand alone.CORS is not a complete CSRF defense. Some cross-origin requests can be sent without CORS permission even when JavaScript cannot read the response.
XSS can often defeat CSRF protections because malicious script running within the trusted origin can access application capabilities. Both vulnerabilities must be addressed.
See the OWASP Cross-Site Request Forgery Prevention Cheat Sheet.
Finished reviewing this question?087
Why is using eval dangerous?
Short answereval()parses and executes a string as JavaScript code.It is dangerous because untrusted input can become executable code, it makes program behavior harder to analyze, and it prevents many engine and build-time optimizations.
Why it mattersNever pass user-controlled or externally controlled content to
eval.eval(userInput); // DangerousPrefer normal JavaScript operations, data parsing, or explicit mappings.
More details
Code injection can occur when data is combined with executable source:
const expression = getUserInput(); const result = eval(expression);An attacker may supply code that reads page data, changes application behavior, or performs authenticated actions.
Safer alternatives depend on the intended task.
Parse JSON with:
const data = JSON.parse(jsonText);Access a dynamic property with bracket notation:
const value = object[propertyName];Select a known operation with a mapping:
const operations = { add: (a, b) => a + b, subtract: (a, b) => a - b }; const operation = operations[operationName]; if (!operation) { throw new Error("Unsupported operation"); } const result = operation(first, second);Set a timer with a function rather than a string:
setTimeout(() => { runTask(); }, 1000);Avoid:
setTimeout("runTask()", 1000);The
Functionconstructor has similar code-generation risks:new Function(userInput);Indirect
evalchanges scope behavior but does not make untrusted code safe.See the MDN
eval()reference.Finished reviewing this question?088
What is strict mode?
Short answerStrict mode is a restricted JavaScript execution mode that converts some silent mistakes into errors, prevents certain unsafe behavior, and simplifies language semantics.
It can be enabled for a classic script or individual function with
"use strict". ES modules and class bodies use strict mode automatically.Why it mattersStrict mode catches accidental global-variable creation:
"use strict"; username = "Alex"; // ReferenceErrorMore details
In non-strict classic code, assigning to an undeclared identifier may create a property on the global object. Strict mode rejects it.
Strict mode also changes standalone function
this:"use strict"; function showThis() { console.log(this); } showThis(); // undefinedWithout strict mode in an older classic browser script,
thismay become the global object.Other strict-mode behaviors include:
- Rejecting assignments to non-writable properties
- Rejecting deletion of plain identifiers
- Preventing duplicate parameter names in ordinary function syntax
- Reserving certain identifiers
- Restricting some
evalbehavior - Removing some historically error-prone semantics
Enable it for an entire classic script by placing the directive first:
"use strict"; function run() { // Strict mode applies here }Or for one function:
function run() { "use strict"; // Strict only inside this function }Blocks cannot independently enable strict mode.
Modern projects using ES modules already receive strict-mode behavior:
<script type="module" src="app.js"></script>See the MDN strict mode reference.
Finished reviewing this question?089
What is optional chaining?
Short answerOptional chaining uses
?.to access a property, call a function, or read an indexed value only when the preceding value is notnullorundefined.If the preceding value is nullish, the expression short-circuits and returns
undefined.Why it mattersIt avoids errors when optional intermediate values may be missing.
const city = user.address?.city;If
user.addressisnullorundefined,citybecomesundefined.More details
Optional property access:
user.profile?.name;Optional element access:
users?.[0]; dictionary?.[key];Optional function call:
options.onComplete?.();Optional chaining checks only for
nullandundefined. Other falsy values remain valid:const data = { count: 0, label: "" }; data.count?.toString(); // "0"Chaining can continue across multiple optional levels:
const country = response.data?.user?.address?.country;Be careful with grouping:
user?.profile.name;This protects access to
user, but ifuserexists andprofileis nullish, accessing.namestill fails. Safer version:user?.profile?.name;Optional chaining cannot be used on the left side of assignment:
user?.name = "Alex"; // SyntaxErrorIt should not hide required-data errors. If a value must exist, explicit validation may communicate the contract better.
Finished reviewing this question?090
What is nullish coalescing?
Short answerThe nullish coalescing operator
??returns its right-hand operand only when its left-hand operand isnullorundefined.Unlike
||, it does not replace other falsy values such as0,false, or an empty string.Why it mattersIt is useful when falsy values are valid data:
const count = 0; count || 10; // 10 count ?? 10; // 0More details
Examples:
null ?? "default"; // "default" undefined ?? "default"; // "default" false ?? true; // false 0 ?? 100; // 0 "" ?? "untitled"; // ""Nullish coalescing is often combined with optional chaining:
const city = user.address?.city ?? "Unknown city";The right-hand expression is evaluated only when needed:
const value = cachedValue ?? calculateValue();calculateValue()runs only ifcachedValueisnullorundefined.Nullish assignment updates a variable only when it is nullish:
settings.theme ??= "light";JavaScript does not allow
??to be mixed directly with&&or||without parentheses:value || fallback ?? defaultValue; // SyntaxErrorThe intended grouping must be explicit:
(value || fallback) ?? defaultValue;Finished reviewing this question?091
What are common ways to test JavaScript code?
Short answerJavaScript code can be tested through automated unit, integration, component, end-to-end, accessibility, performance, and visual tests.
Static analysis, type checking, linting, and manual exploratory testing complement automated tests but test different qualities.
Why it mattersEffective test suites focus on observable behavior and combine fast focused tests with a smaller number of realistic full-system tests.
More details
Common testing layers include:
- Unit tests for isolated logic
- Integration tests for cooperating modules
- Component tests for UI behavior
- End-to-end tests for complete user journeys
- Contract tests for service boundaries
- Visual regression tests for unexpected layout changes
- Accessibility tests for detectable accessibility issues
- Performance tests for speed and resource budgets
A simple behavior-focused test might look like:
function calculateTotal(items) { return items.reduce( (total, item) => total + item.price, 0 ); } test("calculates the total price", () => { const items = [ { price: 10 }, { price: 15 } ]; expect(calculateTotal(items)).toBe(25); });UI tests should generally interact with the interface as a user would:
const button = screen.getByRole("button", { name: "Save" }); await user.click(button); expect( screen.getByText("Saved successfully") ).toBeVisible();Useful testing principles include:
- Test behavior rather than implementation details.
- Keep tests deterministic.
- Avoid unnecessary mocks.
- Test success, failure, empty, and boundary cases.
- Give tests descriptive names.
- Ensure failures clearly identify broken behavior.
- Run important tests automatically in continuous integration.
Automated accessibility tools identify many issues but cannot replace manual keyboard, screen-reader, and usability testing.
Finished reviewing this question?092
What is the difference between unit, integration, and end-to-end testing?
Short answer- Unit tests verify a small piece of logic in isolation.
- Integration tests verify that multiple parts work correctly together.
- End-to-end tests verify complete user workflows through the running application.
The levels differ in scope, realism, speed, maintenance cost, and diagnostic precision.
Why it mattersA balanced test strategy uses each level for the behavior it can verify most effectively.
More details
A unit test might verify a formatting function:
test("formats a full name", () => { expect( formatName({ first: "Alex", last: "Smith" }) ).toBe("Alex Smith"); });Unit tests are typically:
- Fast
- Focused
- Easy to run frequently
- Good at identifying the source of a failure
An integration test might render a form, submit it, and verify its interaction with a mocked or local API layer:
test("submits valid profile data", async () => { render(<ProfileForm />); await user.type( screen.getByLabelText("Name"), "Alex" ); await user.click( screen.getByRole("button", { name: "Save" }) ); expect(api.saveProfile).toHaveBeenCalledWith({ name: "Alex" }); });Integration tests cover boundaries that isolated unit tests may miss.
An end-to-end test might:
- Open the application in a real browser.
- Sign in.
- Add an item to a cart.
- Complete checkout.
- Verify the confirmation page.
End-to-end tests provide strong confidence in important user journeys but are generally slower, more expensive to maintain, and harder to debug.
Not every function needs a unit test, and not every variation needs an end-to-end test. High-value business rules benefit from focused tests, while critical user journeys benefit from a smaller number of reliable end-to-end tests.
Finished reviewing this question?
08Practical interview questions12 questions
093
How would you reverse a string?
Short answerFor simple strings, convert the string to an array, reverse the array, and join it again.
function reverseString(value) { return [...value].reverse().join(""); }Why it mattersStrings are immutable, so the function creates and returns a new string.
reverseString("hello"); // "olleh"More details
Spread syntax handles Unicode code points better than
split(""):[..."A😀B"]; // ["A", "😀", "B"]By comparison,
split("")separates the emoji into UTF-16 code units:"A😀B".split(""); // ["A", "\uD83D", "\uDE00", "B"]A manual implementation can use a loop:
function reverseString(value) { let result = ""; for (const character of value) { result = character + result; } return result; }Both versions have linear time complexity,
O(n), and require additional memory.Some visible characters consist of multiple Unicode code points, such as letters with combining marks or family emojis. Correctly reversing user-perceived characters requires grapheme segmentation:
function reverseGraphemes(value, locale = "en") { const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" }); return [...segmenter.segment(value)] .map(segment => segment.segment) .reverse() .join(""); }In an interview, clarify whether the expected solution must handle basic characters, Unicode code points, or complete grapheme clusters.
Finished reviewing this question?094
How would you check whether a string is a palindrome?
Short answerNormalize the string according to the requirements and compare it with its reversed form.
function isPalindrome(value) { const normalized = value .toLowerCase() .replace(/[^a-z0-9]/g, ""); return ( normalized === [...normalized].reverse().join("") ); }Why it mattersA palindrome reads the same forward and backward.
isPalindrome("Racecar"); // true isPalindrome("Hello"); // falseMore details
A two-pointer solution avoids creating a reversed string:
function isPalindrome(value) { const normalized = value .toLowerCase() .replace(/[^a-z0-9]/g, ""); let left = 0; let right = normalized.length - 1; while (left < right) { if (normalized[left] !== normalized[right]) { return false; } left++; right--; } return true; }Time complexity is
O(n). The normalization step still creates an additional string.Requirements should be clarified:
- Should letter casing be ignored?
- Should spaces and punctuation be ignored?
- Should accented characters be normalized?
- Should all Unicode letters and numbers be supported?
- Is an empty string considered a palindrome?
A Unicode-aware normalization can use property escapes:
const normalized = value .normalize("NFC") .toLocaleLowerCase() .replace(/[^\p{L}\p{N}]/gu, "");This retains Unicode letters and numbers instead of only ASCII characters.
Finished reviewing this question?095
How would you flatten a nested array?
Short answerUse
flat(Infinity)when arrays may have any nesting depth.function flatten(array) { return array.flat(Infinity); }Why it mattersflatcreates a new array and removes the specified number of nested array levels.flatten([1, [2, [3, 4]]]); // [1, 2, 3, 4]More details
A recursive implementation can demonstrate the underlying algorithm:
function flatten(array) { const result = []; for (const value of array) { if (Array.isArray(value)) { result.push(...flatten(value)); } else { result.push(value); } } return result; }A reducer version is also possible:
function flatten(array) { return array.reduce((result, value) => { if (Array.isArray(value)) { return result.concat(flatten(value)); } result.push(value); return result; }, []); }Deep recursion may exceed the call stack. An iterative solution avoids that risk:
function flatten(array) { const stack = [...array].reverse(); const result = []; while (stack.length > 0) { const value = stack.pop(); if (Array.isArray(value)) { stack.push(...value.slice().reverse()); } else { result.push(value); } } return result; }Time complexity is
O(n), wherenincludes all nested values. The result requiresO(n)additional space.Finished reviewing this question?096
How would you group an array of objects by a property?
Short answerIterate over the array and store each object in a collection associated with its property value.
function groupBy(items, property) { return items.reduce((groups, item) => { const key = item[property]; groups[key] ??= []; groups[key].push(item); return groups; }, {}); }Why it mattersEach property value becomes a key whose value is an array of matching objects.
groupBy( [ { name: "Alex", role: "admin" }, { name: "Sam", role: "user" }, { name: "Lee", role: "admin" } ], "role" );More details
The result is:
{ admin: [ { name: "Alex", role: "admin" }, { name: "Lee", role: "admin" } ], user: [ { name: "Sam", role: "user" } ] }A callback makes the function more flexible:
function groupBy(items, getKey) { return items.reduce((groups, item) => { const key = getKey(item); groups[key] ??= []; groups[key].push(item); return groups; }, {}); } groupBy(users, user => user.role);A
Mapsupports keys of any type and avoids object-key coercion:function groupByMap(items, getKey) { const groups = new Map(); for (const item of items) { const key = getKey(item); const group = groups.get(key) ?? []; group.push(item); groups.set(key, group); } return groups; }Modern JavaScript environments may provide
Object.groupBy()andMap.groupBy():const groups = Object.groupBy( users, user => user.role );Check runtime compatibility when using newer built-in methods.
Finished reviewing this question?097
How would you implement debounce?
Short answerReturn a wrapper function that resets a timer after every call. Execute the original function only when no new call occurs during the delay.
function debounce(callback, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { callback.apply(this, args); }, delay); }; }Why it mattersDebouncing is useful when only the final call matters, such as waiting for a user to stop typing before searching.
const search = debounce(fetchResults, 300); input.addEventListener("input", search);More details
applypreserves the wrapper’s dynamicthisand forwards its arguments.A version with cancellation and flushing:
function debounce(callback, delay) { let timeoutId; let latestArgs; let latestThis; function debounced(...args) { latestArgs = args; latestThis = this; clearTimeout(timeoutId); timeoutId = setTimeout(() => { timeoutId = undefined; callback.apply(latestThis, latestArgs); }, delay); } debounced.cancel = () => { clearTimeout(timeoutId); timeoutId = undefined; latestArgs = undefined; latestThis = undefined; }; debounced.flush = () => { if (timeoutId === undefined) { return; } clearTimeout(timeoutId); timeoutId = undefined; callback.apply(latestThis, latestArgs); }; return debounced; }Important design choices include:
- Leading or trailing execution
- Preserving
this - Forwarding arguments
- Returning results
- Cancellation
- Maximum wait time
- Handling async callbacks
A production implementation should also clean up pending timers when the owning UI component is removed.
Finished reviewing this question?098
How would you implement throttle?
Short answerReturn a wrapper that permits the original function to execute at most once during each specified interval.
function throttle(callback, interval) { let lastExecutionTime = 0; return function (...args) { const currentTime = Date.now(); if ( currentTime - lastExecutionTime >= interval ) { lastExecutionTime = currentTime; callback.apply(this, args); } }; }Why it mattersThrottling is useful when regular updates are needed during a frequent event such as scrolling, resizing, or pointer movement.
More details
The basic implementation executes on the leading edge but may discard the final call.
A leading-and-trailing implementation preserves a final call:
function throttle(callback, interval) { let lastExecutionTime = 0; let timeoutId; let latestArgs; let latestThis; return function (...args) { const currentTime = Date.now(); const remaining = interval - (currentTime - lastExecutionTime); latestArgs = args; latestThis = this; if (remaining <= 0) { clearTimeout(timeoutId); timeoutId = undefined; lastExecutionTime = currentTime; callback.apply(latestThis, latestArgs); } else if (timeoutId === undefined) { timeoutId = setTimeout(() => { timeoutId = undefined; lastExecutionTime = Date.now(); callback.apply(latestThis, latestArgs); }, remaining); } }; }Debounce and throttle solve different problems:
- Debounce waits for activity to stop.
- Throttle produces periodic updates while activity continues.
For visual updates,
requestAnimationFramecan act as a rendering-aware throttle:let frameRequested = false; window.addEventListener("scroll", () => { if (frameRequested) { return; } frameRequested = true; requestAnimationFrame(() => { updateInterface(window.scrollY); frameRequested = false; }); });Finished reviewing this question?099
How would you implement a deep clone?
Short answerIn modern JavaScript, use
structuredClone()when the input contains supported structured-clone values.const copy = structuredClone(original);A custom implementation is necessary only when the application has specific cloning rules or unsupported values.
Why it mattersA deep clone creates independent nested data, unlike spread syntax, which copies only the top level.
const copy = structuredClone(original); copy.settings.theme = "dark";More details
structuredClone()supports many built-in types:- Objects and arrays
DateMapSet- Typed arrays
ArrayBuffer- Circular references
It does not clone every kind of value. Functions and DOM nodes are notable unsupported examples.
A basic custom implementation:
function deepClone(value, seen = new WeakMap()) { if ( value === null || typeof value !== "object" ) { return value; } if (seen.has(value)) { return seen.get(value); } if (value instanceof Date) { return new Date(value.getTime()); } if (value instanceof Map) { const copy = new Map(); seen.set(value, copy); for (const [key, item] of value) { copy.set( deepClone(key, seen), deepClone(item, seen) ); } return copy; } if (value instanceof Set) { const copy = new Set(); seen.set(value, copy); for (const item of value) { copy.add(deepClone(item, seen)); } return copy; } const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Reflect.ownKeys(value)) { copy[key] = deepClone(value[key], seen); } return copy; }Even this implementation does not preserve every possible property descriptor, internal slot, or built-in type.
JSON conversion is not a general-purpose deep clone:
JSON.parse(JSON.stringify(value));It loses or changes values such as
undefined,Date,Map,Set,BigInt, symbols, functions, and circular references.In an interview, explain the supported data types before presenting a custom clone.
Finished reviewing this question?100
How would you implement a simplified version of Promise.all?
Short answerConvert the input into an array, resolve each value with
Promise.resolve, store results by input index, and resolve when all operations fulfill.Reject immediately when any input rejects.
function promiseAll(iterable) { const values = Array.from(iterable); return new Promise((resolve, reject) => { if (values.length === 0) { resolve([]); return; } const results = new Array(values.length); let completed = 0; values.forEach((value, index) => { Promise.resolve(value).then(result => { results[index] = result; completed++; if (completed === values.length) { resolve(results); } }, reject); }); }); }Why it mattersResults must remain in input order even when the operations finish in a different order.
More details
Example:
const slow = new Promise(resolve => { setTimeout(() => resolve("slow"), 100); }); const fast = Promise.resolve("fast"); promiseAll([slow, fast, 42]).then(console.log); // ["slow", "fast", 42]The important behaviors are:
- Accept an iterable
- Accept both Promises and ordinary values
- Preserve input order
- Fulfill when every value fulfills
- Reject when the first rejection is observed
- Fulfill with an empty array for empty input
Promise.resolve(value)also adopts compatible thenable values:Promise.resolve({ then(resolve) { resolve("thenable result"); } });Rejecting the combined Promise does not cancel the other operations:
promiseAll([ fetch("/first"), Promise.reject(new Error("Failed")), fetch("/third") ]);The Fetch operations may continue even though the combined Promise has rejected.
A fully specification-compatible implementation has additional details involving iterator behavior, Promise subclasses, thenables, and error handling. Interview implementations are normally expected to demonstrate the core behavior.
Finished reviewing this question?101
How would you retry a failed asynchronous operation?
Short answerWrap the operation in a loop, catch failures, and try again until it succeeds or reaches the maximum number of attempts.
Include a delay, preferably exponential backoff with jitter, and retry only failures that are likely to be temporary.
Why it mattersNot every error should be retried. Validation failures and most client errors will not improve through repetition.
async function retry(operation, attempts = 3) { let lastError; for (let attempt = 1; attempt <= attempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; } } throw lastError; }More details
A more complete implementation:
function wait(milliseconds, signal) { return new Promise((resolve, reject) => { const timeoutId = setTimeout( resolve, milliseconds ); signal?.addEventListener( "abort", () => { clearTimeout(timeoutId); reject(signal.reason); }, { once: true } ); }); } async function retry( operation, { attempts = 3, initialDelay = 300, factor = 2, shouldRetry = () => true, signal } = {} ) { let lastError; for (let attempt = 1; attempt <= attempts; attempt++) { signal?.throwIfAborted(); try { return await operation({ attempt, signal }); } catch (error) { lastError = error; if ( attempt === attempts || !shouldRetry(error) ) { throw error; } const baseDelay = initialDelay * factor ** (attempt - 1); const jitter = Math.random() * baseDelay * 0.2; await wait(baseDelay + jitter, signal); } } throw lastError; }Exponential backoff increases the delay:
300 ms → 600 ms → 1200 msJitter prevents many clients from retrying simultaneously.
Good candidates for retry may include temporary network failures, rate limits, and some server errors. Respect server guidance such as a
Retry-Afterheader.Be careful when retrying non-idempotent operations. Repeating a payment or creation request could duplicate an action unless the API supports idempotency keys.
Finished reviewing this question?102
How would you build an event emitter?
Short answerStore listeners by event name and provide methods to subscribe, unsubscribe, and emit events.
A
Mapof event names toSetcollections prevents duplicate listener entries and supports efficient removal.class EventEmitter { #events = new Map(); on(eventName, listener) { const listeners = this.#events.get(eventName) ?? new Set(); listeners.add(listener); this.#events.set(eventName, listeners); return () => this.off(eventName, listener); } off(eventName, listener) { const listeners = this.#events.get(eventName); if (!listeners) { return; } listeners.delete(listener); if (listeners.size === 0) { this.#events.delete(eventName); } } emit(eventName, ...args) { const listeners = this.#events.get(eventName); if (!listeners) { return; } for (const listener of [...listeners]) { listener(...args); } } }Why it mattersEvent emitters allow components to communicate without requiring the producer to know each consumer directly.
More details
Usage:
const emitter = new EventEmitter(); const unsubscribe = emitter.on( "user:updated", user => { console.log(user.name); } ); emitter.emit("user:updated", { name: "Alex" }); unsubscribe();Copying the listener set during emission provides predictable behavior if a listener removes itself while the event is being processed:
for (const listener of [...listeners]) { listener(...args); }A
oncemethod can automatically unsubscribe:once(eventName, listener) { const unsubscribe = this.on( eventName, (...args) => { unsubscribe(); listener(...args); } ); return unsubscribe; }Design decisions include:
- Should duplicate listeners be allowed?
- Should listener errors stop later listeners?
- Should listeners run synchronously or asynchronously?
- What happens when listeners are added during emission?
- Should wildcard events be supported?
- Is there a maximum-listener warning?
Long-lived emitters can cause memory leaks when consumers forget to unsubscribe. Returning a cleanup function makes correct lifecycle management easier.
Finished reviewing this question?103
How would you cache the result of a function?
Short answerWrap the function with a cache that stores results using the function arguments as keys. When the same input appears again, return the stored result instead of recomputing it.
This technique is called memoization.
function memoize(callback) { const cache = new Map(); return function (argument) { if (cache.has(argument)) { return cache.get(argument); } const result = callback.call(this, argument); cache.set(argument, result); return result; }; }Why it mattersMemoization is most valuable for deterministic, expensive functions that receive repeated inputs.
More details
A multi-argument implementation can use nested maps without converting arguments to strings:
function memoize(callback) { const root = new Map(); const resultSymbol = Symbol("result"); return function (...args) { let current = root; for (const argument of args) { if (!current.has(argument)) { current.set(argument, new Map()); } current = current.get(argument); } if (current.has(resultSymbol)) { return current.get(resultSymbol); } const result = callback.apply(this, args); current.set(resultSymbol, result); return result; }; }Stringifying arguments is simple but unreliable for general values:
const key = JSON.stringify(args);Problems include:
- Property-order differences
- Unsupported values
- Circular references
- Large serialization costs
- Semantically different values producing similar keys
Async functions require special consideration. Caching the Promise prevents duplicate concurrent work:
function memoizeAsync(callback) { const cache = new Map(); return function (key) { if (!cache.has(key)) { const promise = callback.call(this, key); cache.set(key, promise); promise.catch(() => { cache.delete(key); }); } return cache.get(key); }; }Removing rejected Promises allows a later call to retry.
Production caches may also need:
- Expiration
- Size limits
- Least-recently-used eviction
- Manual invalidation
- Weak references for object keys
- Protection against stale data
Memoization should not be used for impure functions whose result changes despite identical arguments.
Finished reviewing this question?104
How would you diagnose and improve a slow frontend application?
Short answerBegin by measuring the user-visible problem, reproduce it consistently, and use browser tools to identify whether the main bottleneck is network loading, JavaScript execution, rendering, memory, or backend latency.
Apply a targeted improvement and measure again to verify the result.
Why it mattersPerformance work should follow evidence:
Measure → identify bottleneck → optimize → verifyA faster internal operation is not valuable unless it improves the user experience or prevents a meaningful resource problem.
More details
Start by clarifying the symptom:
- Is the initial page slow?
- Is an interaction delayed?
- Is scrolling or animation uneven?
- Does performance degrade over time?
- Is the problem limited to certain devices or networks?
- Is the backend response slow?
Useful browser tools include:
- Network panel
- Performance profiler
- Memory profiler
- Rendering diagnostics
- Lighthouse
- Coverage tools
- Framework-specific profilers
- Bundle analyzers
For slow initial loading, inspect:
- Large JavaScript bundles
- Render-blocking resources
- Slow API responses
- Unoptimized images or fonts
- Missing compression or caching
- Excessive third-party scripts
- Duplicate dependencies
Potential improvements include:
- Code splitting
- Lazy loading
- Tree shaking
- Image optimization
- Resource caching
- Preloading critical assets
- Reducing third-party code
- Server-side or static rendering where appropriate
For slow interactions, inspect the main-thread timeline for long tasks:
performance.mark("operation-start"); performOperation(); performance.mark("operation-end"); performance.measure( "operation", "operation-start", "operation-end" );Potential improvements include:
- Reducing unnecessary work
- Breaking long tasks into smaller units
- Moving CPU-heavy work to Web Workers
- Debouncing or throttling frequent events
- Virtualizing large lists
- Avoiding unnecessary component renders
- Caching expensive calculations
For rendering problems, look for:
- Layout thrashing
- Large DOM trees
- Expensive CSS selectors
- Frequent style recalculation
- Large paint areas
- Animations using layout-triggering properties
For degradation over time, inspect:
- Forgotten event listeners
- Active timers
- Detached DOM nodes
- Unbounded caches
- Retained subscriptions
- Repeatedly created resources
Compare results before and after the change using the same conditions. Confirm improvements with real-user monitoring when possible because local development hardware may not represent actual users.
Finished reviewing this question?