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.
Mistake #1: Forgetting Async/Await Error Handling
This mistake has become more common as async/await replaced callback patterns. Developers write clean-looking async code that silently fails in production.
The 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 It Trips 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().
The Fix
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 catchMistake #2: Misunderstanding Closures
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.
The 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 It Trips 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.
The Fix
// 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.
Mistake #3: this Binding Confusion
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.
The 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 It Trips 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.
The Fix
// 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)Mistake #4: Event Loop Misconceptions
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.
The 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 It Trips 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.
The Fix
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.
Mistake #5: Reference vs Value Confusion
Primitives are passed by value. Objects and arrays are passed by reference. This distinction causes subtle bugs that even experienced developers miss.
The 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 It Trips 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.
The Fix
// 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 Table
| 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
If you found this helpful, check out these related guides:
- 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
Ready for More JavaScript Interview Questions?
These five mistakes are just the beginning. Our complete JavaScript interview prep covers 100+ questions on:
- Closures, scope, and hoisting
- Prototypes and inheritance
- ES6+ features (destructuring, modules, generators)
- Async patterns (promises, async/await, observables)
- DOM manipulation and events
- Error handling and debugging
Get Full Access to All JavaScript Questions
Or explore our free JavaScript preview to see more questions like these.
