Top 5 Mistakes Developers Make During JavaScript Interviews (And How to Avoid Them)

·10 min read
javascriptinterview-questionsinterview-tipsfrontendcareerclosuresasync-await

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

  1. Async/await is syntactic sugar over promises—the same error handling rules apply
  2. Unhandled rejections are dangerous—they crash Node.js and cause silent failures in browsers
  3. Check response.ok—fetch doesn't reject on HTTP errors like 404 or 500
  4. 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 catch

Mistake #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

  1. Closures capture variables, not values—the function maintains a reference to the outer scope
  2. let creates block scope—each iteration gets its own binding
  3. var is function-scoped—one variable shared across all iterations
  4. 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

  1. this is determined at call time for regular functions
  2. Arrow functions inherit this from their enclosing scope (lexical binding)
  3. Methods lose context when passed as callbacks
  4. bind, call, apply explicitly set this
// 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, 2

Why 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:

  1. Synchronous code runs first, completely
  2. Microtasks (Promise callbacks, queueMicrotask) run after sync code, before anything else
  3. 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 2

What Interviewers Want to Hear

  1. JavaScript is single-threaded—one call stack, no parallel execution
  2. Microtasks before macrotasks—promises resolve before setTimeout(fn, 0)
  3. Microtask queue drains completely before next macrotask
  4. 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 reference

Why 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

  1. Primitives (string, number, boolean) are copied by value
  2. Objects and arrays are copied by reference—the variable holds a pointer, not the data
  3. Spread operator creates shallow copies—nested objects still share references
  4. 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

MistakeSymptomFix
Missing error handlingSilent failures, crashestry/catch with await, .catch() with promises
Stale closuresWrong values in callbacksUse let, functional updates, or capture values
Lost this contextundefined in callbacksArrow functions, .bind(), or wrapper functions
Wrong async orderPromise before setTimeoutUnderstand microtask vs macrotask queues
Unintended mutationsOriginal data changedSpread operator, concat, Object.assign

Related Articles

If you found this helpful, check out these related guides:


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.


Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides