I've conducted hundreds of JavaScript interviews over 12 years in fintech. The same mistakes keep appearing. Senior developers with 5+ years of experience trip on the same questions that junior developers struggle with. Not because JavaScript is hard to use—because JavaScript is easy to use without understanding how it works.
These five mistakes cost developers job offers. I've seen candidates fail on exactly these points at companies like BNY Mellon, UBS, and major tech firms. Here's how to avoid them.
Table of Contents
- Async/Await Questions
- Closures Questions
- This Binding Questions
- Event Loop Questions
- Reference vs Value Questions
- Quick Reference
Async/Await Questions
This mistake has become more common as async/await replaced callback patterns. Developers write clean-looking async code that silently fails in production.
What is the common async/await mistake?
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
}
// Called somewhere in the app
fetchUserData(123);What happens when the API is down? When the response isn't valid JSON? The promise rejects, and in Node.js, unhandled rejections crash the process. In browsers, failures happen silently.
Why does async/await trip up developers?
Async/await looks synchronous, so developers forget it's still promise-based. They wouldn't write a try/catch around const x = 5, so they don't write one around const data = await fetch().
How should you handle async/await errors?
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch user:', error.message);
// Re-throw, return default, or handle appropriately
throw error;
}
}
// Or handle at call site
fetchUserData(123).catch(error => {
showErrorToUser('Could not load user data');
});What Interviewers Want to Hear
- Async/await is syntactic sugar over promises—the same error handling rules apply
- Unhandled rejections are dangerous—they crash Node.js and cause silent failures in browsers
- Check response.ok—fetch doesn't reject on HTTP errors like 404 or 500
- Error boundaries matter—decide where errors should be caught vs propagated
// Common interview question: What gets logged?
async function example() {
try {
await Promise.reject('error');
console.log('A');
} catch (e) {
console.log('B');
}
console.log('C');
}
example();
// Answer: B, then C
// The rejection is caught, execution continues after catchClosures Questions
Closures are JavaScript's most powerful feature and its most common interview failure point. Developers use closures daily without realizing it, then can't explain why their code behaves unexpectedly.
What is the classic closure mistake?
// Classic loop problem
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
// Prints: 5, 5, 5, 5, 5 (not 0, 1, 2, 3, 4)Why do closures trip up developers?
Developers understand that i changes, but don't understand that the callback closes over the variable, not the value. By the time the timeouts run, the loop has finished and i is 5.
How do you fix closure issues in loops?
// Solution 1: Use let (creates new binding per iteration)
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
// Prints: 0, 1, 2, 3, 4
// Solution 2: IIFE to capture value
for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => {
console.log(j);
}, 100);
})(i);
}
// Solution 3: bind or additional closure
for (var i = 0; i < 5; i++) {
setTimeout(((j) => () => console.log(j))(i), 100);
}What Interviewers Want to Hear
- Closures capture variables, not values—the function maintains a reference to the outer scope
letcreates block scope—each iteration gets its own bindingvaris function-scoped—one variable shared across all iterations- This applies beyond loops—stale closures in React hooks, event handlers, any async callback
// Real-world stale closure (React pattern)
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// Bug: count is always 0 (stale closure)
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // Empty deps = stale closure
// Fix: use functional update
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // Always current
}, 1000);
return () => clearInterval(id);
}, []);
}Deep dive: JavaScript Closures Interview Guide covers scope chains, practical patterns, and more interview questions.
This Binding Questions
JavaScript's this keyword behaves differently than in other languages. It's determined by how a function is called, not where it's defined—unless you use arrow functions.
What is the common this binding mistake?
const user = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};
user.greet(); // "Hello, Alice" ✓
const greetFn = user.greet;
greetFn(); // "Hello, undefined" ✗
setTimeout(user.greet, 100); // "Hello, undefined" ✗Why does this binding trip up developers?
When you pass a method as a callback, it loses its context. The function is called as a plain function, not as a method on the object. In non-strict mode, this becomes the global object; in strict mode, it's undefined.
How do you fix this binding issues?
// Solution 1: Arrow function wrapper
setTimeout(() => user.greet(), 100); // ✓
// Solution 2: bind
setTimeout(user.greet.bind(user), 100); // ✓
// Solution 3: Arrow function in class/object
const user = {
name: 'Alice',
greet: () => {
// Caution: 'this' is lexical, not the object!
console.log(`Hello, ${this.name}`); // Won't work as expected
}
};
// Solution 4: Arrow function as class property
class User {
name = 'Alice';
greet = () => {
console.log(`Hello, ${this.name}`); // ✓ Works!
};
}What Interviewers Want to Hear
thisis determined at call time for regular functions- Arrow functions inherit
thisfrom their enclosing scope (lexical binding) - Methods lose context when passed as callbacks
bind,call,applyexplicitly setthis
// Interview question: What's logged?
const obj = {
value: 42,
getValue: function() {
return this.value;
},
getValueArrow: () => {
return this.value;
}
};
console.log(obj.getValue()); // 42 (method call)
console.log(obj.getValueArrow()); // undefined (lexical this = global/module)
const fn = obj.getValue;
console.log(fn()); // undefined (plain function call)
console.log(fn.call(obj)); // 42 (explicit binding)Event Loop Questions
JavaScript's single-threaded nature and event loop are fundamental to how the language works. Yet most developers can't correctly predict the output of basic async code.
What is the common event loop mistake?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Most developers guess: 1, 4, 2, 3
// Actual output: 1, 4, 3, 2Why does the event loop trip up developers?
Developers know "async code runs later," but don't understand the difference between the microtask queue (promises) and the macrotask queue (setTimeout, I/O). Microtasks always run before macrotasks.
How does the event loop execution order work?
Understand the execution order:
- Synchronous code runs first, completely
- Microtasks (Promise callbacks, queueMicrotask) run after sync code, before anything else
- Macrotasks (setTimeout, setInterval, I/O) run one at a time, with microtasks checked between each
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise inside timeout'));
}, 0);
setTimeout(() => console.log('Timeout 2'), 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('End');
// Output:
// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Promise inside timeout
// Timeout 2What Interviewers Want to Hear
- JavaScript is single-threaded—one call stack, no parallel execution
- Microtasks before macrotasks—promises resolve before setTimeout(fn, 0)
- Microtask queue drains completely before next macrotask
- Blocking the main thread blocks everything—long sync operations freeze the UI
// Why this matters in practice
async function processItems(items) {
for (const item of items) {
await processItem(item); // Yields to event loop between items
}
}
// vs
function processItemsBlocking(items) {
items.forEach(item => {
processItemSync(item); // Blocks entire event loop
});
}Deep dive: JavaScript Event Loop Interview Guide covers the call stack, task queues, and real-world async patterns.
Reference vs Value Questions
Primitives are passed by value. Objects and arrays are passed by reference. This distinction causes subtle bugs that even experienced developers miss.
What is the common reference vs value mistake?
function addItem(arr, item) {
arr.push(item);
return arr;
}
const original = [1, 2, 3];
const result = addItem(original, 4);
console.log(original); // [1, 2, 3, 4] - mutated!
console.log(result); // [1, 2, 3, 4]
console.log(original === result); // true - same referenceWhy does reference vs value trip up developers?
The function looks like it's working with a copy, but it's mutating the original array. This causes bugs in React (state mutations don't trigger re-renders), Redux (state mutations break time-travel debugging), and anywhere immutability is expected.
How do you avoid unintended mutations?
// Solution 1: Spread operator (shallow copy)
function addItem(arr, item) {
return [...arr, item];
}
// Solution 2: concat (returns new array)
function addItem(arr, item) {
return arr.concat(item);
}
// For objects
function updateUser(user, updates) {
return { ...user, ...updates }; // New object
}
// Deep copy when needed
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj)); // Simple but has limitations
// Or use structuredClone(obj) in modern environments
}What Interviewers Want to Hear
- Primitives (string, number, boolean) are copied by value
- Objects and arrays are copied by reference—the variable holds a pointer, not the data
- Spread operator creates shallow copies—nested objects still share references
- Immutability patterns are essential for React, Redux, and functional programming
// Interview gotcha: shallow vs deep copy
const original = {
name: 'Alice',
address: { city: 'NYC' }
};
const shallow = { ...original };
shallow.name = 'Bob'; // Doesn't affect original
shallow.address.city = 'LA'; // DOES affect original!
console.log(original.name); // 'Alice'
console.log(original.address.city); // 'LA' - mutated!
// Deep copy needed for nested objects
const deep = JSON.parse(JSON.stringify(original));
// or: const deep = structuredClone(original);Quick Reference
| Mistake | Symptom | Fix |
|---|---|---|
| Missing error handling | Silent failures, crashes | try/catch with await, .catch() with promises |
| Stale closures | Wrong values in callbacks | Use let, functional updates, or capture values |
Lost this context | undefined in callbacks | Arrow functions, .bind(), or wrapper functions |
| Wrong async order | Promise before setTimeout | Understand microtask vs macrotask queues |
| Unintended mutations | Original data changed | Spread operator, concat, Object.assign |
Related Articles
- Complete Technical Interview Career Guide - comprehensive preparation guide for the entire interview process
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- JavaScript Closures Interview Guide - Deep dive into scope, closures, and practical patterns
- JavaScript Event Loop Interview Guide - Call stack, queues, and async execution
- 12 Tricky JavaScript Interview Questions - More questions that separate juniors from seniors
