Top 65 JavaScript Interview Questions (2026)

The 65 JavaScript questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.

65 questions with answers

What Is JavaScript?

Key Takeaways

  • JavaScript is a high-level, dynamically typed, single-threaded language that runs in browsers and, through Node.js, on servers.
  • It's the language of the web: interactivity in the browser, APIs on the backend, plus tooling, mobile, and desktop through one runtime model.
  • Interviews test how well you understand the runtime (the event loop, closures, this binding, prototypes), not just syntax.
  • This page is a question bank: work your tier first, read one tier up, and say answers out loud because delivery is evaluated too.

JavaScript is a high-level, dynamically typed programming language created by Brendan Eich in 1995, originally to add interactivity to web pages. It's single-threaded and event-driven, uses prototype-based objects, and treats functions as first-class values, which is why it spans procedural, object-oriented, and functional styles. Node.js took the same runtime to the server, so one language now covers browser interactivity, backend APIs, build tooling, and cross-platform apps. In interviews, JavaScript questions probe the runtime model (the event loop, closures, this binding, prototypes, coercion) rather than memorized trivia, because that's where real bugs live. This page collects the 65 questions that come up most, each with a direct answer and runnable code. If you're still building fundamentals, the MDN JavaScript reference from Mozilla is the canonical path; increasingly the first round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.

65Questions with answers on this page
3Difficulty tiers: freshers, intermediate, experienced
22+Runnable code snippets you can practice from
45-60 minTypical length of a JavaScript technical round

Watch: 100+ JavaScript Concepts you Need to Know

Video: 100+ JavaScript Concepts you Need to Know (Fireship, YouTube)

Test yourself and earn a certificate

10 quick questions. Score 70%+ to download your JavaScript certificate.

Jump to quiz

All Questions on This Page

65 questions
JavaScript Interview Questions for Freshers
  1. 1. What is JavaScript and where does it run?
  2. 2. What is the difference between var, let, and const?
  3. 3. What is hoisting?
  4. 4. What is the difference between == and ===?
  5. 5. What are the primitive data types in JavaScript?
  6. 6. What is the difference between undefined and null?
  7. 7. Why does typeof null return "object"?
  8. 8. What is NaN, and how do you check for it?
  9. 9. What is type coercion in JavaScript?
  10. 10. How do you add and remove elements from an array?
  11. 11. What do map, filter, and reduce do?
  12. 12. What are arrow functions and how do they differ from regular functions?
  13. 13. What are template literals?
  14. 14. What is destructuring?
  15. 15. What is the difference between spread and rest operators?
  16. 16. How do the ternary, ?? and ?. operators work?
  17. 17. What does it mean that functions are first-class in JavaScript?
  18. 18. What is a callback function?
  19. 19. What is scope in JavaScript?
  20. 20. What is an IIFE and why was it used?
  21. 21. What is `this` in JavaScript?
  22. 22. How do you select and modify DOM elements?
  23. 23. What do JSON.stringify and JSON.parse do?
  24. 24. What does "use strict" do?
  25. 25. How do default parameters work?
JavaScript Intermediate Interview Questions
  1. 26. What is a closure and why is it useful?
  2. 27. How does the JavaScript event loop work?
  3. 28. What is the difference between microtasks and macrotasks?
  4. 29. What is a Promise and what are its states?
  5. 30. How do async and await work?
  6. 31. What is the difference between Promise.all, Promise.race, Promise.allSettled, and Promise.any?
  7. 32. What is the difference between call, apply, and bind?
  8. 33. What is the prototype chain?
  9. 34. How does prototypal inheritance differ from classical inheritance?
  10. 35. What is event delegation?
  11. 36. What is the difference between debounce and throttle?
  12. 37. What is the difference between ES modules and CommonJS?
  13. 38. What is currying?
  14. 39. What is memoization?
  15. 40. What is the difference between a shallow copy and a deep copy?
  16. 41. What are Set and Map, and how do they differ from arrays and objects?
  17. 42. What are generators and how does yield work?
  18. 43. What is the iterator protocol, and how do you make an object iterable?
  19. 44. What is the difference between forEach, for...of, and for...in?
  20. 45. What is the difference between event bubbling and capturing?
  21. 46. What are Symbols and where are they used?
  22. 47. What is the difference between Array.from and the spread operator for building arrays?
JavaScript Interview Questions for Experienced Developers
  1. 48. Design question: implement Promise.all from scratch.
  2. 49. Design question: implement a deep clone function.
  3. 50. Design question: implement an event emitter (pub/sub).
  4. 51. How does garbage collection work, and how do JavaScript apps leak memory?
  5. 52. What are WeakMap and WeakSet, and when do you use them?
  6. 53. Walk through all the ways `this` gets its value.
  7. 54. How do you handle errors correctly across promises and async/await?
  8. 55. How does tree-shaking work, and what breaks it?
  9. 56. Implement debounce with immediate and cancel options.
  10. 57. Why do closures inside a for loop with var capture the wrong value, and how do you fix it?
  11. 58. Explain the temporal dead zone in detail.
  12. 59. Predict the output: async ordering with await, setTimeout, and promises.
  13. 60. How do you get real private state in JavaScript classes?
  14. 61. What are Proxy and Reflect, and when would you use them?
  15. 62. Implement function composition (compose and pipe).
  16. 63. How do you enforce immutability in JavaScript, and why does it matter?
  17. 64. A JavaScript app is slow or misbehaving in production. Walk through your approach.
  18. 65. Design question: implement your own version of Function.prototype.bind.

JavaScript Interview Questions for Freshers

Freshers25 questions

The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.

Q1. What is JavaScript and where does it run?

JavaScript is a high-level, dynamically typed, single-threaded language that started as the browser's scripting language and now runs on servers through Node.js, plus in mobile and desktop wrappers. It's event-driven, prototype-based, and treats functions as first-class values.

Its reach is the selling point: the browser runs nothing else natively, so front-end interactivity is JavaScript, and Node.js lets you use the same language and skills on the backend. That single-language full-stack story is why it's everywhere.

Key point: A one-line definition plus the browser-and-Node reach beats a memorized feature list. Interviewers open with this to hear how you organize an answer.

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

var is function-scoped and hoisted (initialized to undefined before its line runs). let and const are block-scoped and sit in the temporal dead zone until their declaration, so using them early throws. const can't be reassigned; let can.

const prevents reassignment of the binding, not mutation of the value: a const object can still have its properties changed. Modern code defaults to const, reaches for let when reassignment is needed, and avoids var.

javascript
function demo() {
  console.log(x); // undefined (var is hoisted)
  var x = 1;

  // console.log(y); // ReferenceError (temporal dead zone)
  let y = 2;

  const obj = { a: 1 };
  obj.a = 5;      // fine: mutating, not reassigning
  // obj = {};    // TypeError: reassignment
}
varletconst
ScopeFunctionBlockBlock
HoistingTo undefinedTemporal dead zoneTemporal dead zone
ReassignableYesYesNo
Typical useLegacy codeCounters, reassigned valuesDefault choice

Key point: The follow-up is 'does const make the object immutable?'. Say no, it freezes the binding, and mention Object.freeze for shallow immutability.

Q3. What is hoisting?

Hoisting is how JavaScript moves declarations to the top of their scope during compilation. var declarations and function declarations are hoisted; var is initialized to undefined, function declarations are fully available, and let and const are hoisted but left uninitialized in the temporal dead zone.

So you can call a function declaration before its line but not a function expression assigned to a variable, because only the variable is hoisted, not the value. Understanding this explains a lot of 'undefined' and 'cannot access before initialization' errors.

javascript
greet();            // works: function declaration is hoisted
function greet() { return "hi"; }

// welcome();        // TypeError: welcome is not a function
var welcome = function () { return "hey"; };

Key point: The trap is claiming everything is hoisted equally. Distinguishing function declarations from expressions and the TDZ is what this question screens for.

Q4. What is the difference between == and ===?

=== is strict equality: it compares value and type with no conversion. == is loose equality: it coerces the operands to a common type first, which produces surprising results like 0 == "" being true and null == undefined being true.

The practical rule teams follow: use === (and !==) everywhere, and reach for == only for the deliberate null == undefined check, if at all. Linters flag == for exactly this reason.

javascript
0 == "0";      // true  (coercion)
0 === "0";     // false (different types)

null == undefined;  // true
null === undefined; // false

"" == false;   // true, one of many coercion surprises

Key point: Interviewers love the 0 == "" and [] == false gotchas. Lead with the rule (always use ===), then show you know why == misbehaves.

Q5. What are the primitive data types in JavaScript?

Seven primitives: string, number, bigint, boolean, undefined, null, and symbol. Everything else, including arrays and functions, is an object. Primitives are immutable and compared by value; objects are compared by reference.

Two quirks worth naming: typeof null returns "object" (a historic bug), and numbers are all 64-bit floats, which is why 0.1 + 0.2 isn't exactly 0.3 and why bigint exists for large integers.

javascript
typeof "hi";       // "string"
typeof 42;         // "number"
typeof 10n;        // "bigint"
typeof true;       // "boolean"
typeof undefined;  // "undefined"
typeof null;       // "object"  <- the famous bug
typeof Symbol();   // "symbol"

Q6. What is the difference between undefined and null?

undefined means a variable has been declared but not assigned, or a property or return value is missing; JavaScript sets it automatically. null is an intentional 'no value' that you assign yourself to signal emptiness on purpose.

They're loosely equal (null == undefined is true) but not strictly equal. A clean habit: let the engine produce undefined, and use null when you deliberately want to represent 'nothing here'.

Key point: Expect the follow-up 'why does typeof null return object but typeof undefined return undefined?'. Frame it as a legacy bug that can't be fixed without breaking the web.

Q7. Why does typeof null return "object"?

It's a bug from JavaScript's first implementation. Values were tagged by a small type flag in their low bits, and null was represented as the null pointer (all zeros), which shared the tag used for objects. So typeof reported "object".

It was never fixed because changing it would break countless existing pages. To check for null reliably, compare directly: value === null. This is a favorite because it rewards knowing the language's history, not just its rules.

javascript
typeof null;         // "object"  (the bug)

function isNull(v) {
  return v === null;   // the reliable check
}

Key point: the fix would break backward compatibility. Lead with the identity check.

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

NaN (Not-a-Number) is the numeric result of an invalid math operation, like 0/0 or Number("abc"). It's the only value in JavaScript that isn't equal to itself, so NaN === NaN is false.

Because of that, check with Number.isNaN(value), not === NaN. Avoid the older global isNaN, which coerces its argument first, so isNaN("abc") is true for the wrong reason. Number.isNaN doesn't coerce.

javascript
NaN === NaN;              // false

Number.isNaN(NaN);        // true  (reliable)
Number.isNaN("abc");      // false (no coercion)

isNaN("abc");             // true  (coerces "abc" to NaN first, misleading)

Key point: The 'NaN isn't equal to itself' line always lands. Follow it with Number.isNaN over global isNaN to show current-best-practice awareness.

Q9. What is type coercion in JavaScript?

Coercion is JavaScript automatically converting a value from one type to another. It happens implicitly with operators ("5" + 1 is "51", "5" - 1 is 4) and explicitly when you call Number(), String(), or Boolean().

The + operator prefers string concatenation when either side is a string; other arithmetic operators convert to numbers. Truthiness rules drive if statements: 0, "", null, undefined, NaN, and false are falsy, everything else is truthy.

javascript
"5" + 1;    // "51"  (+ concatenates with a string)
"5" - 1;    // 4     (- forces numbers)
true + 1;   // 2     (true becomes 1)
[] + {};    // "[object Object]"

Boolean("");     // false
Boolean("hi");   // true

Q10. How do you add and remove elements from an array?

push and pop work at the end; unshift and shift work at the front. splice inserts or removes anywhere by index. push, pop, shift, unshift, and splice mutate the array; slice and the spread operator produce a new array without touching the original.

Knowing which methods mutate matters in interviews and in React, where accidental mutation causes stale renders. When in doubt, prefer the non-mutating options.

javascript
const a = [1, 2, 3];
a.push(4);          // [1, 2, 3, 4]
a.pop();            // [1, 2, 3]
a.unshift(0);       // [0, 1, 2, 3]
a.splice(1, 1);     // removes index 1 -> [0, 2, 3]

const copy = [...a, 9]; // new array, original untouched

Q11. What do map, filter, and reduce do?

map transforms each element and returns a new array of the same length. filter keeps elements that pass a test and returns a shorter or equal array. reduce folds the array down to a single value (a sum, an object, another array) using an accumulator.

All three return new values and don't mutate the source, which makes them the backbone of functional-style data transforms. reduce is the general one: you can implement map and filter with it.

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

nums.map(n => n * n);            // [1, 4, 9, 16]
nums.filter(n => n % 2 === 0);  // [2, 4]
nums.reduce((sum, n) => sum + n, 0); // 10

Key point: The follow-up is often 'implement map using reduce'. Being able to do it shows you understand the accumulator, not just the method names.

Watch a deeper explanation

Video: 8 Must Know JavaScript Array Methods (Web Dev Simplified, YouTube)

Q12. What are arrow functions and how do they differ from regular functions?

Arrow functions are a shorter syntax: const add = (a, b) => a + b. The important difference is behavioral, not cosmetic: arrow functions don't have their own this, arguments, or prototype, and can't be used as constructors.

Because they capture this lexically from the surrounding scope, they're ideal for callbacks inside methods where a regular function would rebind this to undefined or the caller. That single trait is why they're everywhere in modern code.

javascript
const square = x => x * x;

const counter = {
  count: 0,
  start() {
    setInterval(() => this.count++, 1000); // arrow keeps this = counter
  },
};

Key point: The bar coverage names the lexical this behavior, not just the shorter syntax. The follow-up is usually 'when would an arrow function be the wrong choice?' (object methods, constructors).

Q13. What are template literals?

Template literals are strings written with backticks that support embedded expressions via ${...} and span multiple lines without escapes. They replace clumsy string concatenation with readable interpolation.

They also enable tagged templates, where a function processes the string parts and interpolated values, used by libraries for styled components, SQL builders, and safe HTML.

javascript
const name = "Asha";
const role = "engineer";

const line = `${name} works as an ${role}.`;
const block = `Hello ${name},
  Welcome aboard.`; // real newline, no \n needed

Q14. What is destructuring?

Destructuring unpacks values from arrays or properties from objects into distinct variables in one statement. Array destructuring goes by position; object destructuring goes by key name and supports defaults and renaming.

It's everywhere in modern JavaScript: pulling props in React, swapping variables without a temp, and reading function results. Combined with the rest pattern, it separates the pieces you want from the ones you don't.

javascript
const [first, second] = [10, 20];
const { name, age = 0 } = { name: "Ben" }; // age defaults to 0

const { id, ...rest } = { id: 1, x: 2, y: 3 }; // rest = { x: 2, y: 3 }

let a = 1, b = 2;
[a, b] = [b, a]; // swap without a temp

Q15. What is the difference between spread and rest operators?

They share the ... syntax but do opposite things. Spread expands an iterable into individual elements: copying arrays, merging objects, passing array items as arguments. Rest collects multiple elements into one array or object: gathering extra function arguments or leftover properties.

The rule of thumb: spread on the right side (building a value), rest on the left side or in a parameter list (collecting values).

javascript
// spread: expand
const merged = [...[1, 2], ...[3, 4]]; // [1, 2, 3, 4]
const clone = { ...{ a: 1 }, b: 2 };   // { a: 1, b: 2 }

// rest: collect
function sum(...nums) {
  return nums.reduce((t, n) => t + n, 0);
}
sum(1, 2, 3); // 6

Key point: Interviewers ask 'is spread a deep copy?'. Answer no: it's shallow, so nested objects are still shared. That leads into the deep-clone question.

Q16. How do the ternary, ?? and ?. operators work?

The ternary condition ? a : b is a compact if/else expression. Nullish coalescing (??) returns the right side only when the left is null or undefined, unlike || which also triggers on any falsy value like 0 or "". Optional chaining (?.) short-circuits to undefined if a reference is null or undefined instead of throwing.

?? and ?. together clean up a lot of defensive code. Reaching for ?? over || is the fix for the classic bug where a valid 0 gets replaced by a default.

javascript
const count = 0;
count || 10;   // 10  <- bug: 0 is falsy
count ?? 10;   // 0   <- correct: 0 isn't null/undefined

const city = user?.address?.city; // undefined instead of a crash

Key point: The ?? vs || distinction with 0 and empty string is a common trap. Volunteering it signals you've hit the bug in real code.

Q17. What does it mean that functions are first-class in JavaScript?

Functions are values: you can assign them to variables, pass them as arguments, return them from other functions, and store them in arrays or objects. This is the foundation for callbacks, higher-order functions, and closures.

Higher-order functions (functions that take or return functions) fall straight out of this, which is why map, filter, and event handlers all accept function arguments. If you understand first-class functions, closures and callbacks stop being mysterious.

javascript
const shout = t => t.toUpperCase();

function apply(fn, value) {
  return fn(value);        // functions passed as arguments
}
apply(shout, "hello");     // "HELLO"

Q18. What is a callback function?

A callback is a function passed to another function to be called later, either when work finishes (async) or during iteration (sync, like the function you give to map). It's the oldest way JavaScript handles 'do this when that's ready'.

The downside of nesting callbacks for sequential async steps is callback hell: deeply indented, hard-to-read pyramids with scattered error handling. Promises and async/await exist largely to fix that.

javascript
function fetchUser(id, callback) {
  setTimeout(() => callback(null, { id, name: "Asha" }), 100);
}

fetchUser(1, (err, user) => {
  if (err) return console.error(err);
  console.log(user.name); // "Asha"
});

Q19. What is scope in JavaScript?

Scope determines where a variable is accessible. There's global scope, function scope (var and function bodies), and block scope (let and const inside {}). Inner scopes can read outer variables, but not the reverse.

Lookups walk outward through the scope chain until they find the name or hit the global scope. This chain, resolved at definition time (lexical scoping), is exactly what makes closures work.

javascript
const outer = "global";

function demo() {
  const inner = "local";
  console.log(outer); // reachable up the chain
}
demo();
// console.log(inner); // ReferenceError: inner is block/function scoped

Q20. What is an IIFE and why was it used?

An IIFE (Immediately Invoked Function Expression) is a function defined and called at once: (function () { ... })(). Before block scoping, it was the main way to create a private scope, avoid polluting the global namespace, and encapsulate variables (the module pattern).

Today let, const, and ES modules cover most of what IIFEs did, so you'll see them more in legacy code. But knowing the pattern and why it existed still comes up.

javascript
const counter = (function () {
  let count = 0;                 // private, not global
  return { next: () => ++count };
})();

counter.next(); // 1
counter.next(); // 2

Key point: If you say IIFEs are mostly historical now, The replaced them (block scope and modules). That indicates current, not dated, knowledge.

Q21. What is `this` in JavaScript?

this refers to the object a function is called on, and its value is decided by how the function is called, not where it's defined. A method call sets this to the object before the dot; a plain function call sets it to undefined in strict mode (or the global object otherwise).

Arrow functions are the exception: they have no this of their own and inherit it lexically. Because this is call-site dependent, the same function can see different this values, which is the source of many bugs.

javascript
const user = {
  name: "Asha",
  greet() { return `Hi, ${this.name}`; },
};
user.greet();          // "Hi, Asha" (this = user)

const fn = user.greet;
fn();                  // this is undefined -> error or "Hi, undefined"

Key point: The classic follow-up: 'what is this when you extract a method and call it bare?'. The answer (it loses its binding) leads straight into call/apply/bind.

Watch a deeper explanation

Video: JavaScript this Keyword (Programming with Mosh, YouTube)

Q22. How do you select and modify DOM elements?

querySelector and querySelectorAll select by CSS selector; getElementById is the fastest for a single id. Once you have an element, you read and write textContent, innerHTML, classList, and attributes, and attach behavior with addEventListener.

Prefer textContent over innerHTML when inserting user data, because innerHTML parses HTML and opens an XSS hole. That safety note is what separates a beginner answer from a job-ready one.

javascript
const btn = document.querySelector(".save");
btn.textContent = "Saved";
btn.classList.add("active");
btn.addEventListener("click", () => console.log("clicked"));

Q23. What do JSON.stringify and JSON.parse do?

JSON.stringify serializes a JavaScript value to a JSON string; JSON.parse turns a JSON string back into a value. They're how you send data over the network, store it in localStorage, and log complex objects.

Watch the edges: stringify drops undefined, functions, and symbols, converts Dates to strings, and throws on circular references. parse gives back plain objects, so a Date comes back as a string unless you revive it.

javascript
const data = { id: 1, when: new Date(), fn: () => {} };
const json = JSON.stringify(data); // fn dropped, date becomes a string
const back = JSON.parse(json);      // { id: 1, when: "2026-..." }

Key point: Interviewers use this as a bridge to deep clone: 'can you deep-clone with JSON?'. Yes for plain data, no for dates, functions, or cycles.

Q24. What does "use strict" do?

Strict mode opts a script or function into a stricter parse and runtime: assigning to an undeclared variable throws instead of creating a global, this is undefined in plain function calls rather than the global object, duplicate parameter names are errors, and several silent failures become loud.

ES modules and class bodies are strict automatically, so you rarely write the directive today, but knowing what it changes (especially the this behavior) explains why modern code catches bugs the old sloppy mode swallowed.

Q25. How do default parameters work?

A default parameter value fills in when an argument is missing or explicitly undefined: function greet(name = "there") uses "there" if you call greet() or greet(undefined). Passing null does not trigger the default, because null is a real value.

Defaults are evaluated at call time, once per call, so a default can reference earlier parameters and even call a function. That's the opposite of a language like Python, where a mutable default is shared, and it's a clean detail to mention.

javascript
function makeUser(name, role = "member", createdAt = Date.now()) {
  return { name, role, createdAt };
}
makeUser("Asha");            // role defaults to "member"
makeUser("Ben", undefined);  // still defaults
makeUser("Cara", null);      // role is null, default skipped

Key point: The 'undefined triggers the default but null doesn't' distinction is the follow-up. It ties back to the undefined-vs-null question.

Back to question list

JavaScript Intermediate Interview Questions

Intermediate22 questions

For candidates with working experience: the runtime model, async control flow, and the questions that separate users from understanders.

Q26. What is a closure and why is it useful?

A closure is a function bundled together with references to the variables in the scope where it was defined. Even after the outer function returns, the inner function keeps access to those variables, because it closed over them.

Closures power private state (a variable no one outside can touch), function factories, memoization, and event handlers that remember context. The classic gotcha is a loop with var: all closures share the one function-scoped variable, so they see its final value. let fixes it by creating a fresh binding per iteration.

javascript
function makeCounter() {
  let count = 0;              // private, kept alive by the closure
  return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2

// the loop gotcha
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3, 3, 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0, 1, 2

Key point: Being able to write makeCounter from memory AND explain the var loop bug is the bar. The two together prove you understand closures, not just define them.

Watch a deeper explanation

Video: Closures Explained in 100 Seconds // Tricky JavaScript Interview Prep (Fireship, YouTube)

Q27. How does the JavaScript event loop work?

JavaScript runs on a single thread with a call stack. When async work (a timer, a fetch, an event) finishes, its callback is queued rather than run immediately. The event loop constantly checks: if the call stack is empty, it takes the next callback from a queue and runs it.

The subtlety is two queues. Microtasks (promise callbacks, queueMicrotask) have priority and drain completely before any macrotask (setTimeout, setInterval, I/O). So a promise .then scheduled after a setTimeout still runs first. This ordering is what the next question drills into.

One turn of the event loop

1Run current task
execute synchronous code until the stack empties
2Drain microtasks
all promise .then / catch callbacks, to completion
3Render (in browsers)
style, layout, paint if needed
4Next macrotask
one setTimeout / setInterval / I/O callback, then repeat

Microtasks always fully drain before the next macrotask, which is why promise callbacks beat setTimeout(0).

Key point: 'Is JavaScript single-threaded?' is the setup. Yes, one call stack, but the runtime offloads timers and I/O, so it feels concurrent. That framing works well.

Q28. What is the difference between microtasks and macrotasks?

Macrotasks include setTimeout, setInterval, and I/O callbacks; microtasks include promise .then/.catch/.finally callbacks, queueMicrotask, and async/await continuations. After each macrotask, the event loop empties the entire microtask queue before running the next macrotask.

That's why the output ordering below surprises people: the promise callback runs before the setTimeout callback even though the timer was scheduled first. Getting this ordering right is a favorite senior screen.

javascript
console.log("1");
setTimeout(() => console.log("2"), 0);   // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");

// Output: 1, 4, 3, 2

Task ordering: microtasks drain before the next macrotask

Relative priority for the snippet above. Lower runs sooner. Sync code first, then all microtasks, then one macrotask.

console.log sync
1 order
Promise .then
10 order
setTimeout(0)
1,000 order
  • console.log sync: runs immediately on the call stack
  • Promise .then: microtask: drains fully before any timer
  • setTimeout(0): macrotask: waits until microtasks are empty

Key point: Interviewers write four log statements and ask for the order. Practice this pattern until the microtask-before-macrotask rule is automatic.

Q29. What is a Promise and what are its states?

A Promise is an object representing the eventual result of an async operation. It has three states: pending (still running), fulfilled (resolved with a value), and rejected (failed with a reason). Once settled, it can't change state again.

You consume it with .then for success, .catch for errors, and .finally for cleanup, and chain them because each returns a new promise. Promises solved callback hell by flattening nested callbacks into a readable chain with one error path.

javascript
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(100)
  .then(() => fetch("/api/user"))
  .then(res => res.json())
  .then(user => console.log(user))
  .catch(err => console.error(err))
  .finally(() => console.log("done"));

Key point: The follow-up is 'what does a .then callback return, and how does chaining work?'. Say each .then returns a new promise, and returning a value or a promise feeds the next link.

Watch a deeper explanation

Video: JavaScript Promise in 100 Seconds (Fireship, YouTube)

Q30. How do async and await work?

An async function always returns a promise. await pauses that function until the awaited promise settles, then resumes with the resolved value, so asynchronous code reads top-to-bottom like synchronous code. Under the hood it's promises plus the microtask queue, not new threads.

Errors are handled with ordinary try/catch instead of .catch, which is cleaner. The trap is awaiting in a loop when the operations are independent: that serializes them. Use Promise.all to run independent awaits concurrently.

javascript
async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    return await res.json();
  } catch (err) {
    console.error("failed", err);
  }
}

// concurrent, not serial
const [a, b] = await Promise.all([loadUser(1), loadUser(2)]);

Key point: The strong coverage names the serial-await-in-a-loop trap and fixes it with Promise.all. That distinction is exactly what 'intermediate' screens for.

Watch a deeper explanation

Video: JavaScript Async Await (Web Dev Simplified, YouTube)

Q31. What is the difference between Promise.all, Promise.race, Promise.allSettled, and Promise.any?

Promise.all resolves with an array of all results, but rejects as soon as any input rejects. Promise.allSettled always resolves, giving you a status/value or status/reason for every promise. Promise.race settles with the first promise to settle, success or failure. Promise.any resolves with the first fulfillment and only rejects if all reject.

Pick by intent: all when you need every result and want to fail fast, allSettled when partial success is fine, race for timeouts, any for 'first success wins'.

MethodResolves whenRejects when
Promise.allAll fulfillAny rejects (first rejection)
Promise.allSettledAll settleNever (each result reports its own status)
Promise.raceFirst settles (fulfill)First settles (reject)
Promise.anyFirst fulfillsAll reject (AggregateError)

Key point: The design follow-up is often 'implement Promise.all yourself'. That question lives in the experienced tier below; know both the behavior table and the implementation.

Q32. What is the difference between call, apply, and bind?

All three set this explicitly. call invokes the function immediately with this and arguments listed individually. apply is the same but takes arguments as an array. bind returns a new function with this (and optionally leading arguments) permanently fixed, to call later.

Mnemonic: call for Comma-separated args, apply for an Array, bind to Build a bound copy. bind is the one you reach for when passing a method as a callback that must keep its this.

javascript
function greet(greeting) { return `${greeting}, ${this.name}`; }
const user = { name: "Asha" };

greet.call(user, "Hi");        // "Hi, Asha"
greet.apply(user, ["Hello"]);  // "Hello, Asha"
const bound = greet.bind(user);
bound("Hey");                  // "Hey, Asha"

Key point: The practical follow-up is 'why use bind in a class or event handler?'. Answer: to lock this before the method is passed somewhere that would rebind it.

Q33. What is the prototype chain?

Every object has an internal link ([[Prototype]], reachable via Object.getPrototypeOf) to another object, its prototype. When you read a property, JavaScript checks the object, then its prototype, then that prototype's prototype, up the chain until it finds the key or reaches null.

This is how inheritance works without classes: methods live on a shared prototype so instances don't each carry a copy. Array methods like map live on Array.prototype; your arrays borrow them through the chain.

javascript
const animal = { speak() { return "..."; } };
const dog = Object.create(animal);   // dog's prototype is animal
dog.speak();                          // "..." found up the chain

Object.getPrototypeOf(dog) === animal; // true

Key point: Expect 'how do classes relate to prototypes?'. Say class is syntax sugar over prototypes: methods still land on the prototype, not each instance.

Q34. How does prototypal inheritance differ from classical inheritance?

Classical inheritance (Java, C++) copies structure from classes into instances at construction. Prototypal inheritance links objects to other objects: an instance delegates to its prototype at lookup time, so behavior is shared by reference, not copied.

The class keyword added familiar syntax, but it's still delegation underneath: extends sets up the prototype chain, and super calls walk it. Saying 'JavaScript classes are sugar over prototypes' is the answer the key point is.

javascript
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
  speak() { return `${this.name} barks`; } // overrides via the chain
}
new Dog("Rex").speak(); // "Rex barks"

Q35. What is event delegation?

Event delegation attaches one listener to a parent element instead of many listeners on children, relying on event bubbling: an event fired on a child propagates up to ancestors. In the handler you inspect event.target to see which child triggered it.

It's efficient (one listener, not hundreds) and it handles elements added later automatically, which plain per-element listeners can't. It's the standard pattern for lists, tables, and dynamic UIs.

javascript
document.querySelector("#list").addEventListener("click", (e) => {
  const item = e.target.closest("li");
  if (item) console.log("clicked", item.dataset.id);
});
// works even for <li>s added after this listener was attached

Key point: The follow-up is 'bubbling vs capturing'. Bubbling (child to parent) is the default; pass { capture: true } for top-down. Delegation relies on bubbling.

Q36. What is the difference between debounce and throttle?

Both limit how often a function runs during rapid events. Debounce waits until activity stops: it runs the function only after a quiet gap, so a search box fires once the user pauses typing. Throttle runs at most once per interval no matter how many events arrive, so a scroll handler fires every 100ms rather than on every pixel.

Rule of thumb: debounce for 'do it when they're done' (search, resize-finished), throttle for 'do it at a steady rate' (scroll, drag, mousemove). Both are closures that hold a timer between calls.

javascript
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

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

Key point: Being able to write debounce from memory is a common live-coding ask. Note that it's a closure over timer, which ties back to the closures question.

Watch a deeper explanation

Video: Learn Debounce And Throttle In 16 Minutes (Web Dev Simplified, YouTube)

Q37. What is the difference between ES modules and CommonJS?

CommonJS (require, module.exports) is Node's original synchronous module system: modules load at runtime and exports are a live object you mutate. ES modules (import, export) are the standard: statically analyzable, so bundlers can tree-shake, and imports are read-only live bindings resolved before execution.

ESM works in browsers and Node; CommonJS is Node-only. The friction points are that ESM is async and top-level await capable, imports are hoisted, and mixing the two needs care (dynamic import() to load CommonJS from ESM).

CommonJSES Modules
Syntaxrequire / module.exportsimport / export
LoadingSynchronous, at runtimeStatic, resolved before run
BindingsCopied value snapshotLive read-only bindings
Tree-shakingHardSupported
WhereNode onlyBrowsers and Node

Key point: The follow-up is 'why can bundlers tree-shake ESM but not CommonJS?'. Because ESM imports are static: the tooling knows what's used before running anything.

Q38. What is currying?

Currying transforms a function that takes multiple arguments into a chain of functions that each take one argument. add(a, b, c) becomes add(a)(b)(c). It's built on closures: each returned function remembers the arguments captured so far.

The practical payoff is partial application: fix some arguments now, supply the rest later, which makes small reusable configured functions. Libraries use it for point-free, composable pipelines.

javascript
const multiply = a => b => c => a * b * c;
multiply(2)(3)(4); // 24

const double = multiply(2);   // partial application
const doubleTriple = double(3);
doubleTriple(5);              // 30

Key point: Interviewers sometimes ask you to write a generic curry() that works for any arity. That's the harder version; start by explaining the closure that captures earlier args.

Q39. What is memoization?

Memoization caches a function's results keyed by its inputs, so repeated calls with the same arguments return the stored answer instead of recomputing. It trades memory for speed and only works for pure functions (same input, same output, no side effects).

It's a closure holding a cache (often a Map). The classic demo is recursive Fibonacci, which goes from exponential to linear once results are cached. React's useMemo and useCallback are the same idea applied to renders.

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

Key point: The constraint out loud: memoization is only safe for pure functions. That caveat is what turns a rote answer into an understanding one.

Q40. What is the difference between a shallow copy and a deep copy?

A shallow copy duplicates only the top level: spread ({...obj}) and Object.assign copy references to nested objects, so mutating a nested value shows up in both copies. A deep copy recursively duplicates everything, producing fully independent structures.

For deep cloning, structuredClone is the modern built-in (handles dates, maps, cycles). JSON.parse(JSON.stringify(x)) is a common quick hack but drops functions, undefined, and dates and throws on cycles. This gap is a favorite bug source.

javascript
const original = { user: { name: "Asha" } };
const shallow = { ...original };
shallow.user.name = "Ben";
original.user.name; // "Ben"  <- shared nested object

const deep = structuredClone(original); // independent

Key point: The follow-up 'implement your own deep clone' shows up in the experienced tier. Here, knowing structuredClone exists is the current-best-practice signal.

Q41. What are Set and Map, and how do they differ from arrays and objects?

A Set stores unique values with O(1) add, delete, and has, so it's the clean way to dedupe or test membership. A Map stores key/value pairs where keys can be any type (including objects), preserves insertion order, and has a real size, unlike plain objects whose keys are strings or symbols.

Reach for Map over an object when keys aren't strings, when you need frequent adds and deletes, or when you want a reliable size and iteration order without prototype-key surprises.

javascript
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1, 2, 3]

const seen = new Map();
const keyObj = {};
seen.set(keyObj, "value"); // object as a key, impossible with plain objects
seen.get(keyObj);          // "value"

Q42. What are generators and how does yield work?

A generator function (function*) can pause and resume. yield produces a value and suspends execution; calling next() resumes right after the last yield, keeping all local state. It returns an iterator, so you can loop it or pull values one at a time.

Generators are lazy, which makes them good for infinite sequences, custom iteration, and streaming large data without holding it all in memory. They also underpin how some async patterns and libraries model cooperative pausing.

javascript
function* idMaker() {
  let id = 1;
  while (true) yield id++; // infinite, but lazy
}
const gen = idMaker();
gen.next().value; // 1
gen.next().value; // 2

Key point: The link interviewers probe is generators to iterators: a generator is the easiest way to implement the iterator protocol, which is the next question.

Q43. What is the iterator protocol, and how do you make an object iterable?

An iterable is any object with a Symbol.iterator method that returns an iterator; an iterator is an object with a next() method returning { value, done }. Arrays, strings, Sets, and Maps are iterable, which is why for...of and spread work on them.

To make your own object iterable, add a Symbol.iterator method. The easiest correct implementation is a generator, because it produces { value, done } and handles done for you.

javascript
const range = {
  from: 1, to: 3,
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) yield n;
  },
};
[...range];              // [1, 2, 3]
for (const n of range) console.log(n); // 1 2 3

Q44. What is the difference between forEach, for...of, and for...in?

for...in iterates enumerable keys, including inherited ones, so it's for objects (and risky on arrays because it yields index strings and can pick up added properties). for...of iterates values of any iterable and supports break, continue, and await. forEach runs a callback per array element but can't break and skips holes.

Rule of thumb: for...of for arrays and other iterables when you might break, forEach for a quick side-effecting loop, and for...in only for plain object keys (guarded with hasOwnProperty).

javascript
const arr = ["a", "b"];
arr.forEach((v, i) => console.log(i, v)); // 0 a, 1 b
for (const v of arr) console.log(v);     // a, b (break-able)
for (const k in { x: 1, y: 2 }) console.log(k); // x, y (keys)

Key point: The trap is using for...in on arrays. If you say 'for...in is for object keys, for...of is for values', you've already sidestepped the mistake they're probing.

Q45. What is the difference between event bubbling and capturing?

When an event fires, it travels in three phases: capturing (top-down from the document to the target), the target itself, then bubbling (bottom-up from the target back to the document). Listeners run in the bubbling phase by default; pass { capture: true } to run them on the way down.

You control propagation with stopPropagation (stop the event traveling further) and preventDefault (cancel the browser's default action, like a form submit). Event delegation relies on bubbling, which is why one parent listener can handle clicks on any child.

javascript
parent.addEventListener("click", () => console.log("parent"));
child.addEventListener("click", (e) => {
  console.log("child");
  // e.stopPropagation(); // would stop "parent" from logging
});
// clicking child logs: child, then parent (bubbling)

Key point: The follow-up pairs stopPropagation with preventDefault. Keep them straight: one stops travel through the DOM, the other cancels the browser's default behavior.

Q46. What are Symbols and where are they used?

A Symbol is a unique, immutable primitive created with Symbol("description"). Every symbol is distinct even with the same description, so symbols make collision-proof object keys: a symbol property won't clash with any string key or any other symbol, and it stays out of normal enumeration and JSON.

Their main real-world role is well-known symbols that hook into language behavior: Symbol.iterator makes an object iterable, Symbol.asyncIterator for async iteration, Symbol.toPrimitive for coercion. Libraries also use symbols to attach hidden metadata that won't interfere with user keys.

javascript
const id = Symbol("id");
const user = { name: "Asha", [id]: 123 };
user[id];                     // 123
Object.keys(user);            // ["name"] <- symbol key hidden
Symbol("id") === Symbol("id"); // false, always unique

Q47. What is the difference between Array.from and the spread operator for building arrays?

Both turn an iterable into an array, and spread ([...set]) is shorter for that case. Array.from does more: it also works on array-like objects that have a length but aren't iterable (like the old arguments object or a NodeList in older environments), and it takes a mapping function as a second argument, so you can transform while converting.

Array.from({ length: n }, (_, i) => i) is the idiomatic way to build a range, something spread can't do alone. Use spread for iterables you just want to copy, Array.from when you need the map step or array-like support.

javascript
Array.from("abc");                       // ["a", "b", "c"]
Array.from({ length: 3 }, (_, i) => i);   // [0, 1, 2]
[...new Set([1, 1, 2])];                  // [1, 2] (spread, dedupe)
Array.from(new Set([1, 1, 2]));           // [1, 2] (same result)

Key point: The distinguishing detail is the mapping second argument and array-like support. If you only ever use spread, this question probes whether you know when it falls short.

Back to question list

JavaScript Interview Questions for Experienced Developers

Experienced18 questions

advanced rounds probe internals, design implementations, and production scars. Expect every answer here to draw a follow-up.

Q48. Design question: implement Promise.all from scratch.

Return a new promise. Track how many inputs have resolved and collect results in an array at the correct index (order must match the input, not completion order). Resolve the outer promise when the count reaches the input length; reject immediately on the first rejection.

The details interviewers watch for: preserving order by index rather than pushing, handling an empty input array (resolve with []), and wrapping each input in Promise.resolve so non-promise values work too.

javascript
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let remaining = promises.length;
    if (remaining === 0) return resolve(results);
    promises.forEach((p, i) => {
      Promise.resolve(p).then(
        (value) => {
          results[i] = value;      // index preserves order
          if (--remaining === 0) resolve(results);
        },
        reject                     // first rejection wins
      );
    });
  });
}

Key point: The order-by-index detail is the whole point. Candidates who push results in completion order fail the follow-up test with out-of-order timers.

Q49. Design question: implement a deep clone function.

Recurse through the structure: primitives return as-is, arrays and plain objects get rebuilt with each value cloned, and you must handle a WeakMap of already-seen objects to survive circular references. Special types (Date, Map, Set, RegExp) need their own branches.

The production-ready coverage names structuredClone as the built-in you'd actually use, then explains why the hand-rolled version needs the cycle guard and type handling that JSON.parse(JSON.stringify(x)) skips.

javascript
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== "object") return value;
  if (seen.has(value)) return seen.get(value); // circular guard
  if (value instanceof Date) return new Date(value);
  const copy = Array.isArray(value) ? [] : {};
  seen.set(value, copy);
  for (const key of Reflect.ownKeys(value)) {
    copy[key] = deepClone(value[key], seen);
  }
  return copy;
}

Key point: Mentioning the WeakMap cycle guard in practice is the strongest single signal here. It's the detail naive implementations forget, and it stack-overflows without it.

Q50. Design question: implement an event emitter (pub/sub).

Keep a map from event name to an array (or Set) of listener callbacks. on adds a listener, off removes it, and emit calls every listener for that event with the passed arguments. once wraps a listener so it removes itself after firing.

The edges worth handling: return an unsubscribe function from on for easy cleanup, copy the listener list before emitting so a listener that unsubscribes mid-emit doesn't corrupt iteration, and isolate listener errors so one throw doesn't stop the rest.

javascript
class EventEmitter {
  constructor() { this.events = new Map(); }
  on(name, fn) {
    if (!this.events.has(name)) this.events.set(name, new Set());
    this.events.get(name).add(fn);
    return () => this.off(name, fn); // unsubscribe handle
  }
  off(name, fn) { this.events.get(name)?.delete(fn); }
  emit(name, ...args) {
    [...(this.events.get(name) ?? [])].forEach(fn => fn(...args));
  }
}

Key point: The 'copy listeners before emitting' detail matters: without it, a once listener that removes itself while you iterate can skip the next listener. The it.

Q51. How does garbage collection work, and how do JavaScript apps leak memory?

JavaScript engines use tracing garbage collection: starting from roots (the global object, the current call stack), the collector marks everything reachable and sweeps the rest. Objects are freed when nothing reachable references them, so you never free manually.

Leaks come from unintended references that keep things reachable: forgotten setInterval timers, event listeners never removed, closures that capture large objects, detached DOM nodes still referenced in JavaScript, and unbounded caches or global arrays that only grow. The fix is removing the reference (clearInterval, removeEventListener) or using WeakMap/WeakRef so entries don't pin their keys alive.

Key point: the question needs the leak categories (listeners, timers, closures, detached nodes) and the diagnosis path (heap snapshots in DevTools, diff over time), not a GC algorithm lecture.

Q52. What are WeakMap and WeakSet, and when do you use them?

WeakMap keys and WeakSet members are held weakly: if nothing else references a key, the garbage collector can reclaim it and the entry disappears. They only accept objects as keys/members, aren't iterable, and have no size, precisely because their contents can vanish at any time.

Use them to attach private data or metadata to objects without preventing those objects from being collected: caching computed results keyed by an object, tracking DOM nodes, or the seen-map in a deep clone. When the object dies, its associated data is cleaned up automatically, so you can't leak through them.

javascript
const privateData = new WeakMap();
class Account {
  constructor(balance) { privateData.set(this, { balance }); }
  getBalance() { return privateData.get(this).balance; }
}
// when an Account instance is unreachable, its private entry is GC'd too

Key point: The tie-in interviewers like: WeakMap can't leak the way a plain Map can, because holding an object as a key in a Map keeps it alive forever. That's the whole reason it exists.

Q53. Walk through all the ways `this` gets its value.

Five rules, in priority order. new binding: calling with new sets this to the fresh object. Explicit binding: call, apply, and bind set this directly. Implicit binding: obj.method() sets this to obj. Default binding: a plain call sets this to undefined in strict mode (or the global object otherwise). Arrow functions ignore all of these and take this lexically from where they're defined.

The bugs come from losing implicit binding: extract a method into a variable or pass it as a callback and the dot is gone, so it falls back to default binding. The fixes are bind, an arrow wrapper, or a class field arrow.

javascript
const obj = {
  val: 42,
  regular() { return this.val; },
  arrow: () => this.val, // this is NOT obj (lexical, likely undefined)
};
obj.regular();               // 42
const loose = obj.regular;
loose();                     // undefined (lost implicit binding)
obj.regular.call({ val: 9 }); // 9 (explicit)

Key point: The precedence order (new > explicit > implicit > default, with arrows outside the whole system). Reciting it in order is what separates a memorized answer from a mental model.

Q54. How do you handle errors correctly across promises and async/await?

With async/await, wrap awaited calls in try/catch, and remember that an error in one awaited call skips the rest of the try block. With raw promises, attach .catch at the end of the chain, because one .catch handles rejections from every prior link. A rejection with no handler becomes an unhandledrejection event.

The senior details: Promise.all fails fast on the first rejection, so use allSettled when you need every outcome; always attach a rejection handler to fire-and-forget promises or they throw unhandled; and in Node, an unhandled rejection can crash the process depending on config.

javascript
async function load() {
  try {
    const [a, b] = await Promise.all([fetchA(), fetchB()]);
    return process(a, b);
  } catch (err) {
    logger.error(err); // catches either fetch or process throwing
    throw err;         // rethrow so callers can react
  }
}

Q55. How does tree-shaking work, and what breaks it?

Tree-shaking is dead-code elimination that bundlers do by statically analyzing ES module imports and exports. Because import/export are static (resolved before code runs), the bundler can see which exports are actually used and drop the rest from the output.

It breaks on side effects: a module that mutates globals on import can't be safely removed, so mark packages sideEffects: false in package.json when they're pure. CommonJS resists tree-shaking because require is dynamic, and re-exporting an entire namespace or importing for side effects also defeats it.

Key point: The follow-up 'why can't CommonJS tree-shake?' has a one-line answer: require runs at runtime, so the tooling can't know statically what's used. Say that and you've nailed it.

Q56. Implement debounce with immediate and cancel options.

The base debounce is a closure over a timer that resets on each call. To support leading-edge (fire immediately, then ignore until quiet) add a flag that tracks whether you're in a fresh burst. To support cancel, expose a method that clears the pending timer so a queued call never fires.

Getting the trailing versus leading distinction right, and exposing cancel for cleanup (in React effects, for instance), is what turns the textbook version into the production version.

javascript
function debounce(fn, delay, { leading = false } = {}) {
  let timer = null;
  function debounced(...args) {
    const callNow = leading && !timer;
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!leading) fn(...args);
    }, delay);
    if (callNow) fn(...args);
  }
  debounced.cancel = () => { clearTimeout(timer); timer = null; };
  return debounced;
}

Key point: Exposing debounced.cancel in practice signals real-world use. Anyone who has cleaned up a debounced handler in a React effect has needed exactly that.

Q57. Why do closures inside a for loop with var capture the wrong value, and how do you fix it?

var is function-scoped, so every iteration shares one binding. The callbacks all close over that single variable, and by the time they run (after the loop) it holds the final value. So three setTimeout logs print 3, 3, 3, not 0, 1, 2.

let creates a fresh block-scoped binding per iteration, so each closure captures its own copy. Before let, the fix was an IIFE that passed the current value in as an argument, creating a new scope each pass.

javascript
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0 1 2

// pre-let fix: IIFE captures a fresh binding
for (var i = 0; i < 3; i++) {
  (function (j) { setTimeout(() => console.log(j)); })(i);
}

Key point: This is the single most-asked closure question. Explaining why (one shared var binding) beats reciting the let fix, because the why is what they're testing.

Q58. Explain the temporal dead zone in detail.

let and const are hoisted to the top of their block, so the name exists throughout the block, but they're not initialized until execution reaches the declaration. The span from the block's start to that line is the temporal dead zone, and accessing the variable there throws a ReferenceError.

This is deliberate: it turns silent bugs (reading a var as undefined before assignment) into loud errors, and it's why typeof on a TDZ variable also throws, unlike typeof on a truly undeclared name, which returns "undefined".

javascript
{
  // console.log(x); // ReferenceError: Cannot access 'x' before initialization
  // typeof x;       // also throws inside the TDZ
  let x = 5;
  console.log(x);    // 5
}

Q59. Predict the output: async ordering with await, setTimeout, and promises.

Synchronous code runs first, to completion. Then the microtask queue drains fully: promise .then callbacks and the code after each await. Only then does one macrotask (a setTimeout callback) run. So awaits and .then always beat setTimeout(0), and the code right after an await is itself a microtask.

Reasoning through this out loud, labeling each statement as sync, microtask, or macrotask, is the skill being tested. Rote memorization of one example doesn't transfer; the model does.

javascript
console.log("A");
setTimeout(() => console.log("B"), 0);
(async () => {
  console.log("C");
  await null;
  console.log("D"); // microtask after await
})();
Promise.resolve().then(() => console.log("E"));
console.log("F");
// Output: A, C, F, D, E, B

Key point: Practice narrating: which line is sync, which schedules a microtask, which schedules a macrotask. Interviewers care that you can derive the order, not recall it.

Q60. How do you get real private state in JavaScript classes?

Modern JavaScript has hard private fields with a # prefix (#balance): they're accessible only inside the class body, invisible to Reflect and JSON, and throw if accessed from outside. This is real encapsulation enforced by the language, unlike the old underscore convention that was privacy by politeness.

Before # fields, the patterns were closures (private variables in the constructor) and WeakMap keyed by instance. Those still work and are worth knowing, but # is the current answer, plus static private members and private methods.

javascript
class Account {
  #balance = 0;                 // truly private
  deposit(n) { this.#balance += n; return this; }
  get balance() { return this.#balance; }
}
const a = new Account();
a.deposit(100).balance; // 100
// a.#balance;          // SyntaxError from outside the class

Q61. What are Proxy and Reflect, and when would you use them?

A Proxy wraps an object and intercepts fundamental operations (get, set, has, deleteProperty) through trap functions, letting you customize behavior: validation on assignment, logging, reactive updates, negative array indices, default values for missing keys. Reflect provides the default implementations of those operations, so a trap can do its custom work and then forward to Reflect.get or Reflect.set for the normal behavior.

Proxies power reactive frameworks (Vue 3's reactivity is built on them) and mocking libraries. The caveat: they add overhead and can't intercept everything, so reach for them when you genuinely need to trap operations, not for ordinary getters and setters.

javascript
const validated = new Proxy({}, {
  set(target, key, value) {
    if (key === "age" && value < 0) throw new RangeError("age >= 0");
    return Reflect.set(target, key, value); // default behavior
  },
});
validated.age = 30;   // ok
// validated.age = -1; // RangeError

Q62. Implement function composition (compose and pipe).

compose(f, g, h) returns a function that applies right to left: compose(f, g)(x) is f(g(x)). pipe is the same but left to right, which reads more naturally as a data pipeline. Both are one-line reduces over the function list.

They lean on first-class functions and are the backbone of point-free, functional-style code. the check is that you're comfortable treating functions as data and reasoning about evaluation order.

javascript
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const inc = n => n + 1;
const double = n => n * 2;
compose(inc, double)(5); // inc(double(5)) = 11
pipe(inc, double)(5);    // double(inc(5)) = 12

Key point: The reduce vs reduceRight direction is the detail to get right. compose is right-to-left (math convention), pipe is left-to-right (reading convention).

Q63. How do you enforce immutability in JavaScript, and why does it matter?

Object.freeze prevents adding, removing, or changing an object's own properties, but it's shallow, so nested objects stay mutable unless you freeze recursively. In practice teams get immutability through discipline and non-mutating operations: spread and destructuring to build new objects, map/filter over splice, and libraries like Immer that produce new state from a mutation-style draft.

It matters most in state management: React and Redux rely on reference equality to detect changes cheaply, so mutating state in place causes missed re-renders and subtle bugs. Immutable updates keep change detection correct and time-travel debugging possible.

javascript
const state = Object.freeze({ user: { name: "Asha" }, count: 1 });
// state.count = 2;          // silently ignored (throws in strict mode)
state.user.name = "Ben";     // still works: freeze is shallow

const next = { ...state, count: state.count + 1 }; // immutable update

Key point: The React angle is the payoff: mutating state in place breaks reference-equality change detection. Connecting immutability to why re-renders fire is the production signal.

Q64. A JavaScript app is slow or misbehaving in production. Walk through your approach.

Observe first: reproduce with real data, then use DevTools (Performance panel for long tasks and layout thrash, Memory panel with heap-snapshot diffs for leaks) or server-side profiling for Node. Check the usual suspects: unbounded re-renders, listeners and timers never cleaned up, synchronous work blocking the main thread, N+1 network calls, and large bundles inflating load.

Then fix at the right layer with a measurement to confirm, not a guess: memoize the hot computation, debounce the noisy handler, virtualize the long list, code-split the heavy route. The structure, observe, hypothesize, verify, fix, confirm, matters more than any one tool.

Key point: The methodology is; naming the exact DevTools panel is supporting evidence, not the answer.

Q65. Design question: implement your own version of Function.prototype.bind.

bind returns a new function whose this is fixed to a given object, with optional leading arguments pre-filled. So your polyfill captures the original function (this inside the polyfill), stores the bound context and partial arguments, and returns a wrapper that calls the original with the fixed context plus the partials and any later arguments.

The detail that separates a correct answer: a bound function used with new should ignore the bound this and construct normally, so a full implementation checks whether it was called as a constructor. For most interviews the apply-based version below is enough, but naming the new-target edge case earns the senior nod.

javascript
Function.prototype.myBind = function (context, ...boundArgs) {
  const fn = this;                       // the original function
  return function (...callArgs) {
    return fn.apply(context, [...boundArgs, ...callArgs]);
  };
};

function greet(greeting) { return `${greeting}, ${this.name}`; }
const bound = greet.myBind({ name: "Asha" }, "Hi");
bound(); // "Hi, Asha"

Key point: The follow-up 'what if someone calls the bound function with new?' tests whether you know bind's constructor behavior. Even naming the edge case, without coding it, indicates depth.

Back to question list

Why JavaScript? JavaScript vs TypeScript, Python, and Java

JavaScript wins when you need one language everywhere the web reaches: the browser runs nothing else natively, and Node.js extends it to servers and tooling. It trades compile-time safety for reach and speed of iteration, so teams ship features fast and hire from a huge pool. Where it bites is the same dynamism: silent type coercion, undefined creeping through, and async control flow that surprises people. TypeScript answers the safety gap without leaving the ecosystem, which is why most serious teams now write TypeScript that compiles to JavaScript. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.

LanguageTypingBest atWatch out for
JavaScriptDynamicBrowser, full-stack web, fast iterationCoercion quirks, undefined bugs, async surprises
TypeScriptStatic (compiles to JS)Large JS codebases that need safetyBuild step, type gymnastics on edge cases
PythonDynamicData, ML, scripting, backendNot native in browsers, GIL for CPU parallelism
JavaStaticLarge enterprise systems, AndroidVerbosity, slower to prototype

How to Prepare for a JavaScript Interview

Prepare in layers, and practice out loud. Most JavaScript rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.

  • Master your tier's concepts until you can explain them without notes, then read one tier up for the stretch questions.
  • Type and run every snippet in the browser console or Node; modifying working code cements it far faster than reading.
  • Practice thinking aloud on small problems with a timer, because your process, not just the final answer is the technical point.
  • Take the quiz on this page to find weak spots, then revisit those questions before the real round.

The typical JavaScript interview flow

1Recruiter or phone screen
background, motivation, a few concept checks
2Core concepts
closures, this, the event loop, promises, prototypes
3Live coding
write and explain a function under observation
4Design or debugging
implement Promise.all, debounce, read unfamiliar code

Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.

Test Yourself: JavaScript Quiz

Ready to test your JavaScript knowledge?

10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.

10 questions Instant feedback Free certificate on 70%+

Frequently  Asked  Questions

Which JavaScript topics should I prioritize before a technical round?

Prioritize the areas that change real decisions: fundamentals, practical tasks, debugging, trade-offs, and evidence. The highest-value questions are tied to a project example, a validation check, or a failure mode.

Are these questions enough to pass a JavaScript interview?

They cover the question-answer portion well, but most JavaScript rounds also include live coding: writing a function under observation while explaining your thinking. solving small problems out loud with a timer is the practice path. Our AI coding interview prep guide covers that format, including how automated evaluation reads your process.

Should I learn TypeScript for a JavaScript interview?

Know JavaScript deeply first, because TypeScript compiles down to it and every type bug is still a runtime bug underneath. If the role lists TypeScript, be ready to explain what static types buy you and where they don't help. But the closures, this binding, and async questions here matter for both, and they're what most interviews lean on.

How long does it take to prepare for a JavaScript interview?

If you use JavaScript at work or in study, one to two weeks of an hour a day covers this bank with practice runs. Starting colder, plan three to four weeks and write code daily. Reading answers without typing them is how preparation quietly fails, especially for async and this, which only click once you've broken them yourself.

Do interviewers ask exactly these questions?

The concepts repeat far more reliably than the exact wording. Interviewers rephrase, chain follow-ups, and push toward whatever you claim confidence in. Prepare the underlying ideas, closures, the event loop, this binding, prototypes, coercion, and the phrasing takes care of itself.

Is there a way to test my JavaScript knowledge?

Yes. The quiz on this page scores you as you go and explains every answer, so you find your weak spots fast. Pass the threshold and you can download a shareable certificate with your name and score, free and with no sign-up. It's the quickest way to check yourself before the real round.

From the team that builds coding interviews

Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages, JavaScript included. These questions reflect what actually gets asked and evaluated inside the interviews we host for 5,000+ HR teams.

See how the AI Coding Interviewer works

Sources

Adithyan RKWritten by Adithyan RK
Surya N
Fact-checked by Surya N
Published on: 9 May 2026Last updated: 14 Jul 2026
Share: