Interview Prep — JavaScript Track

JavaScript Interview Questions

15 JavaScript questions — closures, the event loop, this binding, prototypal inheritance, and async/await. The concepts frontend and full-stack interviewers probe hardest. Tagged for FAANG and startups.

CoreAsyncAdvanced
30Total
6Core
4Async
5Advanced
Progress saved locally. Check off questions — saves to your browser. No account needed.
0
/ 15 Done
01What is the difference between var, let, and const?JS▾
varletconst
ScopeFunctionBlockBlock
HoistingHoisted, init undefinedHoisted to TDZHoisted to TDZ
ReassignYesYesNo
RedeclareYesNoNo
TDZ (Temporal Dead Zone): let/const are hoisted but not initialised. Accessing them before their declaration throws ReferenceError rather than returning undefined. Default to const, use let when reassigning, avoid var.
02What is the difference between == and ===?JS▾
== (loose) coerces both operands to the same type before comparing. === (strict) compares value and type with no coercion.
javascript
1 == "1"        // true  — string coerced to number
1 === "1"       // false — different types
0 == false      // true
null == undefined   // true  — special-cased
null === undefined  // false
NaN === NaN      // false — NaN is never equal to anything
Rule: always use === unless you have a specific reason to want coercion. It avoids a whole class of subtle bugs.
03What is a closure? Give a practical use.JS▾
A closure is a function that retains access to variables from the scope where it was created, even after that outer function has returned. It's how JavaScript gives you private state without classes.
javascript
function makeCounter() {
  let count = 0;            // private — no outside access
  return () => ++count;
}
const c = makeCounter();
c(); // 1
c(); // 2 — count persists between calls
Uses: data privacy, function factories, memoization, event handlers that remember state, and the module pattern.
04How does this work? How do arrow functions change it?JS▾
this is determined by how a function is called, not where it's defined:
  • obj.method() → this is obj
  • Plain call fn() → undefined in strict mode (global object otherwise)
  • fn.call(x) / apply / bind → explicitly whatever you pass
  • new Fn() → the newly created object
Arrow functions have no this of their own — they capture it lexically from the enclosing scope. That makes them ideal for callbacks but wrong for object methods that need their own this.
javascript
const obj = {
  name: "Ada",
  greet() { return `Hi, ${this.name}`; }
};
const fn = obj.greet;
fn();          // "Hi, undefined" — lost its receiver
obj.greet.bind(obj)();  // "Hi, Ada"
05What are the falsy values in JavaScript?JS▾
There are exactly six (plus document.all historically): false, 0, "" (empty string), null, undefined, and NaN.
Common trap: [], {}, "0", and "false" are all truthy. An empty array in a boolean context is true. Also note 0 == false is true but [] == false is also true due to coercion.
06Explain pass-by-value vs pass-by-reference in JavaScript.JS▾
JavaScript is always pass-by-value — but for objects, the "value" being passed is a reference (a pointer). So primitives are copied, while objects share the same underlying data.
javascript
function mutate(x, obj) {
  x = 99;              // local copy — caller unaffected
  obj.val = 99;        // same object — caller sees it
  obj = {};             // reassigns local ref only
}
let n = 1, o = { val: 1 };
mutate(n, o);
n;      // 1
o.val;  // 99
To copy an object instead of sharing it: spread {...obj} (shallow) or structuredClone(obj) (deep).
07What is hoisting, and why is let different from var?JS▾

Declarations are processed before any code runs. A var is created and initialised to undefined, so reading it early gives undefined. A let or const is created but left uninitialised — the temporal dead zone — and reading it early throws.

console.log(a); var a = 1;    // undefined
console.log(b); let b = 1;    // ReferenceError: Cannot access 'b' before initialization

Function declarations are hoisted whole and callable before their line; function expressions assigned to a variable follow that variable’s rules.

08What is the difference between null and undefined?JS▾

undefined is the absence the language produces: an unassigned variable, a missing parameter, a property that is not there, a function with no return. null is the absence a programmer writes deliberately.

typeof undefined  // "undefined"
typeof null       // "object"  — a bug kept for compatibility
null == undefined   // true  (loose)
null === undefined  // false (strict)

Default parameters and ?? treat only null and undefined as missing, which is what makes them safer than || when 0 or "" is a legitimate value.

09Explain shallow vs deep copy of an object, and how you make each.JS▾

Spread and Object.assign copy one level: nested objects are still shared references, so mutating a nested value shows up in both copies.

const shallow = { ...original };
const deep    = structuredClone(original);   // modern, handles Dates, Maps, cycles
const crude   = JSON.parse(JSON.stringify(original)); // loses Dates, undefined, functions

structuredClone is the right default now. The JSON round-trip is still common but silently mangles anything that is not plain JSON.

10What does Array.prototype.map give you that forEach does not?JS▾

map returns a new array of the results, so it composes: xs.map(f).filter(g). forEach returns undefined and exists only for side effects.

Using map for side effects and ignoring the result allocates an array nobody reads — a reviewer reads it as a mistake. Using forEach to build an array by pushing is the same mistake in reverse.

Neither can be broken out of early. When you need that, use a for...of, or some/find which short-circuit by design.

11What is the difference between slice and splice?JS▾

slice(start, end) returns a copy of a section and leaves the original alone. splice(start, count, ...items) mutates the array in place, removing and inserting, and returns what it removed.

const xs = [1, 2, 3, 4];
xs.slice(1, 3);     // [2, 3]      xs unchanged
xs.splice(1, 2);    // [2, 3]      xs is now [1, 4]

Same story across the array methods: sort, reverse and fill mutate; concat, flat and the newer toSorted/toReversed do not.

12Explain the event loop. What are microtasks vs macrotasks?JS▾
JavaScript is single-threaded. The event loop lets it stay non-blocking: it runs all synchronous code, then drains the entire microtask queue, then runs one macrotask, and repeats.
  • Microtasks: Promise callbacks (.then), queueMicrotask, MutationObserver
  • Macrotasks: setTimeout, setInterval, I/O, UI rendering
javascript
console.log("1");
setTimeout(() => console.log("4"), 0);   // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("2");
// Output: 1, 2, 3, 4
Key insight: a microtask always runs before the next macrotask, even a setTimeout(fn, 0) queued earlier.
13What is a Promise? What are its states?JS▾
A Promise represents a value that may not be ready yet. It is always in one of three states, and once settled it never changes again:
  • Pending — initial, outcome unknown
  • Fulfilled — resolved with a value, triggers .then()
  • Rejected — failed with a reason, triggers .catch()
Promises flatten the "callback hell" of nested callbacks into a chain, with a single .catch() for the whole chain.
14What is async/await and how does it relate to Promises?JS▾
async/await is syntactic sugar over Promises. An async function always returns a Promise; await pauses the function until a Promise settles, without blocking the thread.
javascript
async function load(id) {
  try {
    const res = await fetch(`/api/${id}`);
    if (!res.ok) throw new Error(res.status);
    return await res.json();
  } catch (err) {
    console.error(err);
    return null;
  }
}
Trap: await inside a loop runs sequentially. For concurrency, collect Promises and await Promise.all(...).
15Compare Promise.all, allSettled, race, and any.JS▾
MethodResolves whenRejects when
allEvery promise fulfillsAny single one rejects (fail-fast)
allSettledAll settleNever — always resolves
raceFirst settles (either way)If the first to settle rejects
anyFirst fulfillsAll reject (AggregateError)
Use all when you need every result, allSettled when partial failure is acceptable, race for timeouts, and any for "first success wins".
16What actually happens if you forget await?JS▾

You get the Promise itself, not its value, and the function continues immediately. If the Promise later rejects, nothing is handling it — an unhandled rejection that crashes Node or logs an error nobody attributes to the right line.

const user = getUser();          // a Promise, not a user
if (user.isAdmin) { /* undefined: always falsy */ }

await Promise.all([a(), b()]);   // both start, then both awaited

Starting the calls first and awaiting after is how you run work concurrently on purpose — awaiting each in turn serialises them for no reason.

17How do you handle errors in async code, and what does try/catch miss?JS▾

try/catch around an await catches a rejection. It does not catch a rejection from a Promise you never awaited, nor one thrown inside a callback that runs later, such as a setTimeout body.

try { await risky(); } catch (e) { /* caught */ }

try { risky(); } catch (e) { /* NOT caught: no await */ }
setTimeout(() => { throw new Error(); }, 0);  // NOT caught

For fire-and-forget work, attach .catch() explicitly. At the process edge, listen for unhandledrejection so nothing fails silently.

18What is a race condition in a single-threaded language?JS▾

JavaScript runs one piece of code at a time, but await yields — anything can run between the start and end of an async function. Two overlapping calls can interleave and clobber shared state.

let cache = null;
async function get() {
  if (!cache) cache = await fetchIt();  // two callers both see null and both fetch
  return cache;
}
// fix: cache the Promise, not the value
let inflight = null;
function get() { return inflight ??= fetchIt(); }

The general shape of the fix is to make the in-progress operation itself the thing you store, so the second caller joins the first instead of starting a second.

19How do you cancel an in-flight request?JS▾

Promises are not cancellable, so you cancel the operation behind them with an AbortController. fetch takes its signal and rejects with an AbortError.

const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal })
  .catch(e => { if (e.name !== 'AbortError') throw e; });
ctrl.abort();     // e.g. the user typed again, or the component unmounted

This is what stops a stale search response from overwriting a newer one, and what prevents a state update after a component has gone away.

20What is the difference between setTimeout(fn, 0) and queueMicrotask(fn)?JS▾

queueMicrotask runs in the microtask queue, which drains completely before the next macrotask — the same queue Promise callbacks use. setTimeout(fn, 0) schedules a macrotask, so it runs after the current microtask queue empties and after rendering has had its chance.

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('micro'));
console.log('sync');
// sync, promise, micro, timeout
A microtask that queues another microtask can starve the macrotask queue entirely — the page stops rendering. Use setTimeout when you actually want to yield to the browser.
21Explain prototypal inheritance and the prototype chain.JS▾
Every object has an internal link ([[Prototype]]) to another object. When you access a property JS can't find on the object, it walks up this prototype chain until it finds the property or reaches null. This is how methods are shared without copying them onto every instance.
javascript
class Animal { speak() { return "..."; } }
const dog = new Animal();
dog.speak();        // found on Animal.prototype
Object.getPrototypeOf(dog) === Animal.prototype; // true
class syntax is sugar over this same mechanism — JS has no separate class-based system underneath.
22Implement debounce. How does it differ from throttle?JS▾
Debounce delays running a function until a pause in calls (e.g. search-as-you-type). Throttle runs at most once per interval regardless of call frequency (e.g. scroll handlers).
javascript
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
// input.addEventListener("input", debounce(search, 300));
23What is event delegation and why is it useful?JS▾
Event delegation attaches a single listener to a parent element and uses event bubbling to handle events from its children — including children added later. It's more memory-efficient than one listener per child.
javascript
list.addEventListener("click", (e) => {
  if (e.target.matches(".item")) {
    console.log("clicked", e.target.textContent);
  }
});
24What are the nullish coalescing (??) and optional chaining (?.) operators?JS▾
  • a ?? b — returns b only when a is null or undefined (not for other falsy values like 0 or "", unlike ||).
  • obj?.a?.b — short-circuits to undefined if any link is null/undefined, instead of throwing.
javascript
0 || "default"   // "default" — 0 is falsy
0 ?? "default"   // 0 — only null/undefined trigger fallback
user?.address?.city  // undefined instead of TypeError
25What is currying? Show a simple implementation.JS▾
Currying transforms a function of many arguments into a chain of functions each taking one argument. It enables partial application and reusable, specialised functions.
javascript
const add = (a) => (b) => (c) => a + b + c;
add(1)(2)(3);   // 6

const add5 = add(5);   // partial application
add5(10)(20);     // 35
26What are generators, and what are they actually used for?JS▾

A function* can pause at yield and resume where it left off, producing values lazily. That makes infinite or expensive sequences cheap — you compute only what is consumed.

function* ids() { let i = 0; while (true) yield ++i; }
const it = ids();
it.next().value;   // 1
it.next().value;   // 2

They are the machinery under async/await, and are still the clean way to write custom iterables, paginate an API lazily, or walk a tree without building the whole list.

27What is the module pattern, and how do ES modules differ from CommonJS?JS▾

ES modules are static: import/export are analysed before execution, which is what makes tree-shaking and circular-import detection possible. CommonJS require() is a synchronous function call resolved at runtime, so the dependency graph is only known by running the code.

// ESM — hoisted, live bindings, always strict mode
import { helper } from './helper.js';
export const value = 1;

// CJS — runtime, copies the value at require time
const { helper } = require('./helper');
module.exports = { value: 1 };

ESM bindings are live: reassign an export and importers see the new value. A CommonJS destructure captures a snapshot.

28What is memoisation, and when does it hurt?JS▾

Caching a pure function’s result by its arguments, so a repeat call is a lookup. It pays when the function is expensive and called repeatedly with the same inputs.

const memo = (fn, cache = new Map()) => (...args) => {
  const k = JSON.stringify(args);
  if (!cache.has(k)) cache.set(k, fn(...args));
  return cache.get(k);
};
It hurts when the function is not pure — you cache a stale answer — or when the key space is unbounded, which turns the cache into a memory leak. A plain Map keyed on user input grows forever; bound it, or use a WeakMap keyed on objects so entries can be collected.
29Explain call, apply and bind.JS▾

All three set this. call invokes immediately with arguments listed; apply invokes immediately with arguments as an array; bind invokes nothing and returns a new function with this fixed.

greet.call(user, 'hi');
greet.apply(user, ['hi']);
const bound = greet.bind(user);   // call later

bind is what makes a method safe to pass as a callback in code that predates arrow functions. Note an arrow function ignores all three — its this is captured from the enclosing scope and cannot be reassigned.

30What is the difference between Map and a plain object, and between Set and an array?JS▾

Map takes any value as a key (including objects), keeps insertion order, reports size directly, and has no prototype keys to collide with. An object coerces every key to a string and inherits from Object.prototype.

const m = new Map([[objKey, 1]]);
m.get(objKey);        // 1 — an object as a real key

const s = new Set([1, 1, 2]);
s.has(1);             // O(1); array.includes is O(n)

Reach for Map when keys are dynamic or not strings, and Set for membership tests and de-duplication. A plain object is still right for a fixed, known shape — and it is the only one of the four that serialises to JSON directly.

No questions match your search.