JavaScript interview mistakes usually come from applying a true sentence outside the context where it is true. “Promises run first,” “arrows preserve this,” and “spread copies an object” are useful reminders, not complete execution models.
This 2026 guide focuses on five boundaries that make those reminders precise. For each answer, state the host and mode, identify the binding or queue involved, and describe the observable result before reaching for a fix.
Table of Contents
- Catching errors at the wrong boundary
- Describing closures as frozen values
- Explaining this without the call site
- Reducing the event loop to two queues
- Calling object arguments pass-by-reference
Mistake 1: Catching Errors at the Wrong Boundary
Why is “put try/catch around every await” weak advice?
An async function returns a Promise. An exception thrown in its body or a rejection observed by await rejects that Promise unless a catch handles it. The design question is not whether every line has a catch; it is which boundary owns recovery.
This function catches an error only to lose its stack, cause, and type:
async function loadUser(id) {
try {
return await repository.find(id);
} catch {
throw new Error('Something went wrong');
}
}Catch when the layer can do something meaningful:
async function loadUser(id, {signal}) {
try {
const response = await fetch('/api/users/' + encodeURIComponent(id), {signal});
if (!response.ok) {
throw new HttpError(response.status, await response.text());
}
return await response.json();
} catch (error) {
if (error instanceof HttpError && error.status === 404) {
return null; // This API defines not-found as an expected result.
}
throw new Error('Unable to load user ' + id, {cause: error});
}
}Important distinctions:
fetch()normally rejects for a network/abort failure, not merely because the response is HTTP 404 or 500;awaitpauses only the current async evaluation, not the agent or whole program;- aborting local observation does not guarantee a remote write was undone;
Promise.allrejects when an input rejects, but does not automatically cancel the remaining operations;- sequential
awaitmay be required for dependencies, but independent work may use controlled concurrency.
At the top boundary—a request handler, background job, UI action, or process—record the failure once with useful context and choose an explicit outcome. In Node.js, unhandled-rejection behavior is configurable; current defaults can promote an unhandled rejection to an uncaught exception. Do not make a portable interview answer depend on “it always crashes” or “the browser silently ignores it.”
Mistake 2: Describing Closures as Frozen Values
What does a closure actually retain?
A closure is a function together with access to its surrounding lexical environment. Identifier resolution reaches bindings in that environment. The binding may later contain a different value.
let status = 'pending';
const readStatus = () => status;
status = 'complete';
console.log(readStatus()); // "complete"The classic loop result follows from binding creation rules:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// One var binding; callbacks later read 3, 3, 3.
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0);
}
// A fresh per-iteration binding; callbacks read 0, 1, 2.“Closures capture variables, not values” is closer than “they copy everything,” but still incomplete. A callback can close over a distinct parameter binding whose initial value was supplied at creation time:
const handlers = [];
for (var i = 0; i < 3; i++) {
handlers.push(((snapshot) => () => snapshot)(i));
}Stale data is not a defect inherent to closures. It appears when a long-lived callback reads a binding or snapshot whose update policy does not match the desired behavior. Ask:
- Which environment owns the binding?
- Is a fresh binding created per call or iteration?
- Does the callback need the latest value or the value at registration time?
- What keeps the closure reachable, and when is cleanup required?
The JavaScript closures interview guide covers lexical environments, module state, private data, callbacks, and memory reachability in more depth.
Mistake 3: Explaining this Without the Call Site
Why does an extracted method lose its receiver?
For an ordinary function call, this depends on the call form and mode. Calling object.method() supplies object as the receiver. Extracting the function removes that reference:
const user = {
name: 'Alice',
greet() {
return 'Hello, ' + this.name;
},
};
user.greet(); // receiver: user
const greet = user.greet;
greet(); // plain call
greet.call(user); // explicit receiverIn strict code—including ECMAScript modules—a plain call gives an ordinary function undefined as this. Sloppy script code can substitute globalThis. Therefore, a comment that always predicts “undefined” without naming the mode is unreliable.
Arrow functions do not define their own this binding; they use the surrounding lexical this:
const user = {
name: 'Alice',
bad: () => this.name, // Not dynamically bound to user.
good() {
return () => this.name; // Arrow closes over good()'s this.
},
};
user.good()(); // "Alice"Choose the mechanism from the API contract:
- preserve the method call with a wrapper:
() => user.greet(); - create a stable bound function once with
user.greet.bind(user); - accept a receiver explicitly when object-oriented dispatch adds no value;
- use an arrow field when per-instance identity and lexical
thisare intended.
Also mention trade-offs. Calling bind repeatedly creates new function identities, which matters when removing listeners. Arrow fields are per-instance properties, not prototype methods. call and apply affect a single invocation; bind returns a new function.
Mistake 4: Reducing the Event Loop to Two Queues
Do microtasks always run before tasks?
In the familiar browser example, yes for a specific reason:
console.log('A');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('B');
// A, B, promise, timerThe initial script runs as a task. At the end of that task, the browser performs a microtask checkpoint, so the Promise reaction runs before the event loop selects a later timer task.
The complete browser model is more precise:
- an event loop can have multiple task queues chosen by task source;
- one runnable task is selected and executed;
- microtask checkpoints happen at specified points and drain queued microtasks, including newly queued ones;
- rendering is an opportunity governed by the HTML processing model, not a “render macrotask”;
setTimeout(fn, 0)requests a minimum delay and does not promise immediate or globally ordered execution;- an endless microtask chain can delay tasks and rendering.
function starve() {
queueMicrotask(starve);
}
starve(); // Demonstration only: prevents normal progress.“JavaScript is single-threaded” also needs a boundary. One ECMAScript agent executes one job at a time, but a browser process can use workers and other agents, and host subsystems perform I/O and other work. Shared memory introduces an explicit memory model.
Node.js has a different host event loop with phases and a separate next-tick queue. Do not transfer browser timer ordering claims to top-level Node.js code. The JavaScript event loop interview guide separates browser tasks, microtasks, rendering, and Node.js behavior.
Mistake 5: Calling Object Arguments Pass-by-Reference
How are object arguments passed in JavaScript?
Arguments are values. When the value identifies an object, caller and callee can access the same mutable object:
function update(profile) {
profile.name = 'Alicia'; // Mutates the shared object.
profile = {name: 'Other'}; // Reassigns only the local parameter.
}
const profile = {name: 'Alice'};
update(profile);
console.log(profile.name); // "Alicia"If JavaScript passed the caller's variable by reference, assigning a new object to profile inside update would replace the caller's binding. It does not. “Pass by sharing” is a useful informal label, but the key is to distinguish the variable binding from object identity.
Spread is shallow:
const original = {
name: 'Alice',
address: {city: 'Warsaw'},
};
const copy = {...original};
copy.name = 'Alicia';
copy.address.city = 'Kraków';
console.log(original.name); // "Alice"
console.log(original.address.city); // "Kraków"Spreading a normal object copies enumerable own properties through property access. It does not make a general clone of prototypes, property descriptors, private fields, or internal slots.
structuredClone supports many structured types, circular references, and transferable resources:
const source = {
createdAt: new Date(),
labels: new Map([['priority', 'high']]),
};
source.self = source;
const copy = structuredClone(source);
console.log(copy.self === copy); // trueIt is still not “clone anything”: functions and some platform objects are unsupported, custom class semantics are not preserved as a domain invariant, and transferring an ArrayBuffer detaches it from the source. JSON stringify/parse is serialization with additional data loss and is not a general deep-copy algorithm.
Prefer an explicit domain copy or immutable update when validation, ownership, prototypes, capabilities, or invariants matter.
Quick Reference
| Weak answer | Stronger answer |
|---|---|
“Catch every await.” | “Assign a rejection owner; catch where the layer can recover, translate, compensate, or present.” |
| “Closures capture the value.” | “A closure resolves bindings through a lexical environment; binding creation and update timing determine the result.” |
| “Arrows preserve context.” | “Arrows have lexical this; ordinary functions derive it from call form and strict/sloppy mode.” |
| “Microtasks always beat macrotasks.” | “Name the host, current task, checkpoint, task source, and rendering opportunity.” |
| “Objects are passed by reference; spread deep-copies.” | “Object-identifying values are shared; spread is shallow, and structured clone has defined limits.” |
Frequently Asked Questions
What is the biggest JavaScript interview mistake?
The biggest mistake is repeating a mnemonic without naming its boundary. JavaScript behavior often depends on strict versus sloppy code, script versus module, browser versus Node.js, task source, object identity, and who owns an asynchronous failure. State the environment, predict the observable behavior, explain the specification model, and verify assumptions with a minimal example.
Should every await be wrapped in try catch?
No. Every rejected promise needs an intentional owner, but catching at every await can destroy context or duplicate handling. Catch where you can recover, translate, add useful context, compensate, or present an error; otherwise let the rejection propagate to a request, job, component, or process boundary. Check fetch response status separately because HTTP 404 or 500 normally resolves to a Response.
Do closures capture values or variables?
A closure is a function plus access to its surrounding lexical environment. It resolves bindings from that environment; a later read can observe a later value. A for loop declared with let creates a fresh per-iteration binding, while var uses one function or global-scoped binding. Saying only that closures capture variables or values hides these environment and binding rules.
Do promise callbacks always run before setTimeout callbacks?
In the usual browser example, synchronous script queues both callbacks in one task, then the microtask checkpoint runs the Promise reaction before a timer task is selected. That is not a universal two-queue rule. Browsers have multiple task queues and rendering opportunities, microtasks run at specified checkpoints, and Node.js has its own phases and next-tick behavior.
Are JavaScript objects passed by reference?
JavaScript passes argument values. For an object, that value identifies an object, so caller and callee can hold values that identify the same mutable object. Reassigning the parameter does not reassign the caller's variable, while mutating the shared object is observable through both. This is often called sharing, but it is not pass-by-reference to the caller's variable.
Does the spread operator make a deep copy?
No. Object and array spread create shallow copies: nested object identities remain shared, and object spread does not preserve every prototype, descriptor, private field, or internal slot. structuredClone handles many structured, cyclic values and can transfer supported resources, but it also has unsupported types and different semantics. Choose an explicit domain copy when invariants matter.
Sources
- ECMAScript language specification
- ECMAScript: Async Function Objects
- ECMAScript: Function Definitions
- ECMAScript: Execution Contexts and Jobs
- HTML Standard: Web application APIs and event loops
- HTML Standard: Safe passing of structured data
- Fetch Standard
- Node.js: Errors
- Node.js: Process events
- Node.js: Event loop, timers, and nextTick
Related Articles
- JavaScript Closures Interview Guide - lexical environments, private state, and memory
- JavaScript Event Loop Interview Guide - browser and Node.js scheduling models
- 17 Tricky JavaScript Interview Questions - coercion, scope, equality, and language edge cases
- Complete Frontend Developer Interview Guide - broader frontend interview preparation
