Toolmingo
Guides18 min read

50 JavaScript Interview Questions (With Answers)

Top JavaScript interview questions with clear answers and code examples — covering closures, the event loop, prototypes, async/await, ES6+, and common gotchas.

JavaScript interviews test language fundamentals, async patterns, and problem-solving. This guide covers the 50 most common questions — with concise answers and runnable code.

Quick reference

Topic Most asked questions
Variables var vs let vs const, hoisting, TDZ
Closures Lexical scope, factory functions, loop bug
this Binding rules, arrow vs regular, call/apply/bind
Prototypes Prototype chain, Object.create, class vs prototype
Async Event loop, Promise, async/await, Promise.all
ES6+ Destructuring, spread, generators, optional chaining
Arrays map/filter/reduce, flat, forEach vs map
Error handling try/catch, custom errors, async error handling
Patterns Debounce, throttle, memoize, curry
DOM Event delegation, bubbling, preventDefault

Variables and scope

1. What is the difference between var, let, and const?

Feature var let const
Scope Function Block Block
Hoisted Yes (as undefined) Yes (TDZ) Yes (TDZ)
Reassignable Yes Yes No
Re-declarable Yes No No
Global property Yes (window.x) No No
function example() {
  if (true) {
    var a = 1;   // accessible outside the if block
    let b = 2;   // block-scoped
    const c = 3; // block-scoped, immutable binding
  }
  console.log(a); // 1
  console.log(b); // ReferenceError
}

Rule: always prefer const; use let when you need reassignment; never use var.


2. What is hoisting?

Hoisting is JavaScript's behaviour of moving declarations to the top of their scope before execution.

console.log(x); // undefined (not ReferenceError)
var x = 5;

// The engine sees it as:
var x;
console.log(x); // undefined
x = 5;

let and const are hoisted but not initialised — accessing them before declaration throws a ReferenceError. The gap between entering scope and the declaration is called the Temporal Dead Zone (TDZ).

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;

3. What is the difference between null and undefined?

let a;             // undefined — variable declared but not assigned
let b = null;      // null — intentional absence of value

typeof undefined   // "undefined"
typeof null        // "object" (historical bug in JS)

null == undefined  // true  (loose equality)
null === undefined // false (strict equality)

4. What is the Temporal Dead Zone (TDZ)?

The TDZ is the region between entering a block scope and the point where the let/const variable is declared. Accessing the variable in this zone throws a ReferenceError.

{
  // TDZ starts
  console.log(x); // ReferenceError
  let x = 5;      // TDZ ends here
  console.log(x); // 5
}

Closures

5. What is a closure?

A closure is a function that retains access to variables from its outer (enclosing) scope even after the outer function has returned.

function makeCounter() {
  let count = 0;
  return {
    increment() { count++; },
    decrement() { count--; },
    value()     { return count; }
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
// count is private — not accessible from outside

6. Classic closure bug: var in a loop

// Bug
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3 3 3 — all closures share the same `i`

// Fix 1: use let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0 1 2
}

// Fix 2: IIFE to capture value
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 100))(i); // 0 1 2
}

7. What is a factory function?

A factory function uses closures to create objects with private state.

function createUser(name) {
  let loginCount = 0; // private

  return {
    getName: ()  => name,
    login:   ()  => ++loginCount,
    getStats:()  => ({ name, loginCount })
  };
}

const user = createUser('Alice');
user.login();
user.login();
console.log(user.getStats()); // { name: 'Alice', loginCount: 2 }

this keyword

8. What are the four rules for this?

Rule Example this value
Default binding fn() undefined (strict) / window
Implicit binding obj.fn() obj
Explicit binding fn.call(obj) obj
new binding new Fn() newly created object

Arrow functions do not have their own this — they inherit from the enclosing lexical scope.


9. How do call, apply, and bind differ?

function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!');       // "Hello, Alice!" — args as list
greet.apply(person, ['Hi', '?']);       // "Hi, Alice?"   — args as array
const bound = greet.bind(person, 'Hey'); // returns new function
bound('.');                             // "Hey, Alice."

10. Why does this lose context in callbacks?

const obj = {
  name: 'Alice',
  greet() {
    setTimeout(function () {
      console.log(this.name); // undefined — `this` is the timer context
    }, 100);
  }
};

// Fix 1: arrow function
greet() {
  setTimeout(() => {
    console.log(this.name); // 'Alice' — inherits outer `this`
  }, 100);
}

// Fix 2: bind
greet() {
  setTimeout(function() {
    console.log(this.name);
  }.bind(this), 100);
}

Prototypes

11. What is the prototype chain?

Every object has a hidden [[Prototype]] link. Property lookups travel up the chain until null is reached.

const animal = { breathes: true };
const dog = Object.create(animal);
dog.barks = true;

console.log(dog.barks);   // true  (own property)
console.log(dog.breathes);// true  (from prototype)
console.log(dog.hasOwnProperty('breathes')); // false

12. What is the difference between __proto__ and prototype?

  • __proto__ — the actual prototype link on every object instance.
  • prototype — a property on constructor functions that becomes the __proto__ of instances.
function Dog(name) { this.name = name; }
Dog.prototype.bark = function() { return 'Woof!'; };

const rex = new Dog('Rex');
console.log(rex.__proto__ === Dog.prototype); // true
console.log(rex.bark());                      // 'Woof!'

13. How does class relate to prototypes?

class syntax is syntactic sugar over prototype-based inheritance.

class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  speak() { return `${this.name} barks`; }
}

const d = new Dog('Rex');
d.speak();                  // "Rex barks"
d instanceof Animal;        // true
Object.getPrototypeOf(d) === Dog.prototype; // true

Async JavaScript

14. How does the event loop work?

Call stack → executes synchronous code
Web APIs   → handles async (setTimeout, fetch, etc.)
Task queue → macrotasks (setTimeout callbacks)
Microtask queue → Promises, queueMicrotask (higher priority)

Microtasks are drained before the next macrotask.

console.log('1');

setTimeout(() => console.log('2'), 0);     // macrotask

Promise.resolve().then(() => console.log('3')); // microtask

console.log('4');

// Output: 1, 4, 3, 2

15. What is the difference between a Promise and async/await?

async/await is syntactic sugar over Promises — it makes async code look synchronous.

// Promise chain
fetch('/api/user')
  .then(res => res.json())
  .then(user => console.log(user))
  .catch(err => console.error(err));

// async/await (equivalent)
async function getUser() {
  try {
    const res  = await fetch('/api/user');
    const user = await res.json();
    console.log(user);
  } catch (err) {
    console.error(err);
  }
}

16. What are Promise combinators?

Method Behaviour
Promise.all(arr) Resolves when all resolve; rejects on first rejection
Promise.allSettled(arr) Always resolves with array of {status, value/reason}
Promise.race(arr) Settles as soon as the first one settles
Promise.any(arr) Resolves on first fulfillment; rejects only if all fail
const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
]);

results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value);
  else console.error(r.reason);
});

17. How do you handle errors in async/await?

// Option 1: try/catch
async function load() {
  try {
    const data = await fetchData();
    return data;
  } catch (err) {
    console.error(err);
    return null;
  }
}

// Option 2: .catch() on the awaited promise
async function load() {
  const data = await fetchData().catch(err => {
    console.error(err);
    return null;
  });
  return data;
}

18. What is the difference between setTimeout(fn, 0) and a resolved Promise?

setTimeout(fn, 0) queues a macrotask. A resolved Promise.then() queues a microtask. Microtasks run before macrotasks, so the Promise callback executes first.

setTimeout(() => console.log('macro'), 0);
Promise.resolve().then(() => console.log('micro'));
// Output: micro, macro

ES6+ features

19. What is destructuring?

Destructuring extracts values from arrays and objects into variables.

// Array
const [first, , third] = [1, 2, 3];

// Object with rename and default
const { name: fullName, age = 0 } = { name: 'Alice' };

// Nested
const { address: { city } } = { address: { city: 'Paris' } };

// In function parameters
function display({ name, score = 100 }) {
  return `${name}: ${score}`;
}

20. What is the spread operator vs rest parameters?

// Spread: expand iterable into positions
const a = [1, 2];
const b = [...a, 3, 4];    // [1, 2, 3, 4]
const c = { ...obj, z: 9 }; // shallow copy + override

// Rest: collect remaining args into array
function sum(first, ...rest) {
  return rest.reduce((acc, n) => acc + n, first);
}
sum(1, 2, 3, 4); // 10

21. What is optional chaining (?.)?

Safely accesses deeply nested properties — returns undefined instead of throwing.

const user = { profile: { address: null } };

// Without optional chaining
const city = user && user.profile && user.profile.address && user.profile.address.city;

// With optional chaining
const city = user?.profile?.address?.city; // undefined (no error)

// Works on methods and arrays too
user?.getAvatar?.();
arr?.[0]?.name;

22. What are generators?

Functions that can pause (yield) and resume, producing values lazily.

function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const gen = range(1, 3);
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }

// Useful for infinite sequences
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

23. What is the difference between for...of and for...in?

const arr = ['a', 'b', 'c'];

// for...of — iterates values of any iterable (array, string, Map, Set)
for (const val of arr) console.log(val); // 'a' 'b' 'c'

// for...in — iterates enumerable property keys (avoid on arrays)
for (const key in arr) console.log(key); // '0' '1' '2'
// Also iterates inherited enumerable properties — use hasOwnProperty guard

Arrays

24. What is the difference between map, filter, and reduce?

const nums = [1, 2, 3, 4, 5];

// map — transforms each element, returns same-length array
nums.map(n => n * 2);       // [2, 4, 6, 8, 10]

// filter — keeps elements that pass a test
nums.filter(n => n % 2 === 0); // [2, 4]

// reduce — folds to a single value
nums.reduce((acc, n) => acc + n, 0); // 15

// Chaining
nums
  .filter(n => n % 2 !== 0) // [1, 3, 5]
  .map(n => n ** 2)          // [1, 9, 25]
  .reduce((a, b) => a + b);  // 35

25. What is the difference between forEach and map?

forEach map
Returns undefined New array
Side effects Intended use Avoid
Break early No No
Chainable No Yes

Use map when you need the transformed array; use forEach for side effects only.


26. How do you flatten a nested array?

const nested = [1, [2, [3, [4]]]];

nested.flat();          // [1, 2, [3, [4]]] — depth 1
nested.flat(2);         // [1, 2, 3, [4]]
nested.flat(Infinity);  // [1, 2, 3, 4]

// flatMap (map + flat depth 1)
['hello', 'world'].flatMap(w => w.split('')); // ['h','e','l','l','o','w','o','r','l','d']

Functions

27. What is the difference between a function declaration and a function expression?

// Declaration — hoisted (callable before definition)
sayHi(); // works
function sayHi() { return 'hi'; }

// Expression — not hoisted
greet(); // TypeError: greet is not a function
const greet = function() { return 'hello'; };

// Arrow function expression
const add = (a, b) => a + b;

28. What is currying?

Currying transforms a function with multiple arguments into a chain of single-argument functions.

// Normal
const add = (a, b) => a + b;
add(2, 3); // 5

// Curried
const curriedAdd = a => b => a + b;
const add5 = curriedAdd(5);
add5(3); // 8
add5(10); // 15

// Generic curry
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn(...args);
    return (...more) => curried(...args, ...more);
  };
}

29. What is memoization?

Caching the results of expensive function calls.

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const fib = memoize(function(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
});

fib(40); // fast, computed once

Objects

30. What is the difference between shallow and deep copy?

const original = { a: 1, nested: { b: 2 } };

// Shallow copy — nested object is still shared
const shallow = { ...original };
shallow.nested.b = 99;
console.log(original.nested.b); // 99 (mutated!)

// Deep copy (modern, built-in)
const deep = structuredClone(original);
deep.nested.b = 99;
console.log(original.nested.b); // 2 (safe)

// JSON trick (doesn't handle Date, undefined, functions)
const deep2 = JSON.parse(JSON.stringify(original));

31. What does Object.freeze do?

Makes an object's properties non-writable and non-configurable (shallow).

const config = Object.freeze({ env: 'production', debug: false });
config.debug = true;    // silently fails (error in strict mode)
config.debug;           // false

// Only shallow — nested objects are still mutable
const obj = Object.freeze({ nested: { x: 1 } });
obj.nested.x = 99; // works!

32. What is Object.create?

Creates a new object with a specified prototype.

const proto = {
  greet() { return `Hello, I'm ${this.name}`; }
};

const alice = Object.create(proto);
alice.name = 'Alice';
alice.greet(); // "Hello, I'm Alice"

// Object.create(null) — no prototype at all (pure hash map)
const map = Object.create(null);
// No .toString, .hasOwnProperty etc.

Error handling

33. What types of errors exist in JavaScript?

Error type When thrown
SyntaxError Invalid syntax (parse time)
ReferenceError Accessing undeclared variable
TypeError Wrong type operation (call non-function)
RangeError Number out of valid range
URIError Malformed URI function argument
EvalError Problem with eval() (rare)
null.foo;           // TypeError
undeclaredVar;      // ReferenceError
new Array(-1);      // RangeError

34. How do you create a custom error?

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
    this.name = 'NotFoundError';
  }
}

try {
  throw new NotFoundError('User');
} catch (err) {
  if (err instanceof NotFoundError) {
    console.log(err.statusCode); // 404
  }
}

Common patterns

35. What is debounce? Implement it.

Debounce delays execution until after a pause in calls. Used for search inputs, resize events.

function debounce(fn, delay) {
  let timerId;
  return function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn.apply(this, args), delay);
  };
}

const onSearch = debounce((query) => fetchResults(query), 300);
input.addEventListener('input', e => onSearch(e.target.value));

36. What is throttle? Implement it.

Throttle limits execution to at most once per interval. Used for scroll, mousemove events.

function throttle(fn, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

const onScroll = throttle(() => updateUI(), 100);
window.addEventListener('scroll', onScroll);

37. What is the difference between debounce and throttle?

Debounce Throttle
Executes when After calls stop for delay ms At most once per limit ms
Use case Search input, resize end Scroll, mousemove, resize
First call Delayed Executed immediately

DOM

38. What is event delegation?

Attaching a single listener to a parent to handle events from many children — using event bubbling.

// Without delegation (bad — 100 listeners)
document.querySelectorAll('.btn').forEach(btn =>
  btn.addEventListener('click', handleClick)
);

// With delegation (good — 1 listener)
document.getElementById('list').addEventListener('click', (e) => {
  if (e.target.matches('.btn')) {
    handleClick(e.target);
  }
});

Delegation also works for dynamically added elements — a major advantage.


39. What is the difference between event.target and event.currentTarget?

  • event.target — the element that triggered the event (innermost).
  • event.currentTarget — the element the listener is attached to.
document.getElementById('parent').addEventListener('click', (e) => {
  console.log(e.target);        // clicked child element
  console.log(e.currentTarget); // always #parent
});

40. What is event bubbling and capturing?

Events travel in three phases:

  1. Capture — from window down to the target.
  2. Target — at the target element.
  3. Bubble — from the target back up to window.
// Capture phase (third argument = true)
el.addEventListener('click', handler, true);

// Bubble phase (default)
el.addEventListener('click', handler);

// Stop bubbling
el.addEventListener('click', (e) => {
  e.stopPropagation();
});

Tricky questions

41. What does typeof null === 'object' mean?

It's a historical bug in JavaScript (the value null was stored with the object type tag 000 in the original engine). It was never fixed to avoid breaking existing code. Use === null to check for null.


42. What is NaN and how do you check for it?

NaN (Not a Number) is the result of invalid numeric operations. It is the only value in JS not equal to itself.

typeof NaN;        // "number" (quirk)
NaN === NaN;       // false
Number.isNaN(NaN); // true  (reliable check)
isNaN('hello');    // true  (coerces first — unreliable)
Number.isNaN('hello'); // false (no coercion)

43. What is == vs ===?

== performs type coercion; === requires both value and type to match.

0 == false;     // true  (coercion)
0 === false;    // false
null == undefined;  // true
null === undefined; // false
'5' == 5;       // true
'5' === 5;      // false

Always use === unless you explicitly need coercion.


44. What is the difference between undefined and not defined?

let x;
console.log(x);        // undefined — declared, not assigned
console.log(typeof y); // "undefined" — typeof is safe for undeclared vars
console.log(y);        // ReferenceError — accessing undeclared variable

45. What is the difference between Array.isArray and instanceof Array?

Array.isArray([]);          // true  — reliable across iframes
[] instanceof Array;        // true  — fails across different realms (iframes, vm)
typeof [];                  // "object" — not useful for arrays

// Use Array.isArray — it works in all environments

46. Explain Symbol

Symbol creates a guaranteed unique value — useful for object keys that won't clash.

const id = Symbol('id');
const user = {
  name: 'Alice',
  [id]: 42          // not visible in for...in or JSON.stringify
};

user[id];           // 42
user[Symbol('id')]; // undefined — different symbol

47. What is a WeakMap and when do you use it?

WeakMap stores key-value pairs where keys are objects only and are held weakly (won't prevent garbage collection).

const cache = new WeakMap();

function processUser(user) {
  if (cache.has(user)) return cache.get(user);
  const result = heavyComputation(user);
  cache.set(user, result);
  return result;
}
// When user object is garbage collected, cache entry is too — no memory leak

48. What is the difference between Map and a plain object {}?

Feature Map Object {}
Key types Any value String / Symbol
Order Insertion order Mostly preserved (ES2015+)
Size .size Object.keys().length
Iteration for...of for...in (includes inherited)
Performance Better for frequent add/delete Better for static data
const map = new Map();
map.set({ id: 1 }, 'Alice');    // object as key
map.set(42, 'number key');
map.size;                        // 2

49. What is a Proxy?

Proxy intercepts and customises operations on objects (get, set, delete, etc.).

const handler = {
  get(target, prop) {
    return prop in target ? target[prop] : `Property '${prop}' not found`;
  },
  set(target, prop, value) {
    if (typeof value !== 'number') throw new TypeError('Must be a number');
    target[prop] = value;
    return true;
  }
};

const obj = new Proxy({}, handler);
obj.count = 5;
obj.count;   // 5
obj.foo;     // "Property 'foo' not found"
obj.x = 'a'; // TypeError

50. What is the difference between call stack overflow and maximum call stack size exceeded?

Both refer to stack overflow — too many nested function calls, typically from infinite recursion.

// Causes stack overflow
function recurse() { return recurse(); }
recurse(); // Uncaught RangeError: Maximum call stack size exceeded

// Fix: tail recursion manually (via trampoline) or use iteration
function trampoline(fn) {
  return function(...args) {
    let result = fn(...args);
    while (typeof result === 'function') result = result();
    return result;
  };
}

Common mistakes

Mistake Why it's wrong Fix
Mutating state directly Leads to bugs, breaks React rendering Use spread or structuredClone
typeof null === 'object' JS quirk — null is not an object Use === null to check
[] == false is true Type coercion surprises Use ===
forEach with async Awaits don't work inside forEach Use for...of instead
Adding to prototype Breaks for...in loops for others Use class syntax
Not handling Promise rejection Unhandled rejection crashes Node Always add .catch() or try/catch
Using == instead of === Type coercion causes bugs Default to ===
var in loops Shared binding in closures Use let

FAQ

Q: What's asked most in JavaScript interviews?
A: Closures, the event loop, this binding, promises/async, prototype chain, and common array methods (map/filter/reduce). Know these deeply — they appear in most interviews.

Q: How do I reverse a string in JavaScript?

const rev = str => str.split('').reverse().join('');

For Unicode-aware reversal (emoji, accents): [...str].reverse().join('').

Q: What is the difference between slice and splice?
slice(start, end) returns a new array without modifying the original. splice(start, deleteCount, ...items) modifies the original array and returns removed elements.

Q: What does void 0 do?
It evaluates the expression and returns undefined. Used as a safe undefined (before ES5, undefined was reassignable). Prefer undefined directly in modern code.

Q: How do you check if an object is empty?

Object.keys(obj).length === 0;       // most common
JSON.stringify(obj) === '{}';        // works but slower
Object.entries(obj).length === 0;    // same as keys check

Q: What is the difference between synchronous and asynchronous code?
Synchronous code runs line by line, blocking execution until each operation completes. Asynchronous code (via callbacks, Promises, or async/await) lets the program continue while waiting for I/O operations, handled through the event loop.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools