JavaScript Interview Questions and Answers: 60 Questions from Easy to Hard
This page is for Indian job seekers preparing for JavaScript interviews, from freshers facing their first technical round to experienced developers targeting product companies. It covers 60 questions in three levels: easy questions on core language basics, medium questions on closures, this, promises and the event loop, and hard questions on polyfills, performance and tricky edge cases. Each answer is written the way a strong candidate would say it, with short code you can run in the browser console or Node. Read the answer, try the code, then explain it aloud in your own words.
What JavaScript interviews cover
- Core language rules: types and coercion, == versus ===, scope, hoisting and the temporal dead zone, which almost every first round tests with short output questions.
- Functions in depth: closures, this, call, apply and bind, arrow functions, and writing utilities such as debounce, throttle, curry and memoize by hand.
- Asynchronous code: callbacks, promises, async and await, Promise combinators, and predicting output order from the event loop with microtasks and timers.
- Objects and arrays: prototypes and classes, copying, Map and Set, and modern methods such as map, reduce, toSorted, findLast and structuredClone.
- Browser skills for front-end roles: DOM events, bubbling and delegation, storage options, fetch with cancellation, and avoiding memory leaks and layout thrashing.
How freshers should prepare
- Learn the core rules well enough to explain them aloud: var, let and const, truthy and falsy values, and why typeof null returns object.
- Type every code sample on this page into the browser console or Node and change one thing at a time to see how the output changes.
- Practise ten output-prediction puzzles a day, covering closures in loops, this in callbacks and promise ordering, and write down your reasoning before running them.
- Build one small project, such as a to-do list or a job search page, using fetch, event delegation and localStorage without any framework.
- Write debounce, throttle and a simple Promise.all from memory, because Indian service and product companies ask these often in the first technical round.
How experienced candidates should prepare
- Expect deeper why questions: explain the event loop precisely, compare CommonJS and ES modules, and discuss how closures affect memory in long-lived pages.
- Prepare real stories from your work, such as fixing a race condition, a memory leak or a slow page, with the numbers you measured before and after.
- Practise machine-coding rounds of 60 to 90 minutes: build an autocomplete, an infinite scroll or a star rating widget with clean, tested code.
- Revise polyfills and patterns that senior rounds use, including bind, new, Promise.all, retry with backoff, an LRU cache and an event emitter.
- Know modern tooling and standards: ES2023 array methods, AbortController, structuredClone, and how bundlers use static imports for tree shaking.
Common mistakes to avoid
- Memorising outputs without the rule behind them; interviewers change one line of the puzzle and expect you to reason through the new result.
- Saying JavaScript is multi-threaded or that setTimeout with 0 runs immediately; explain the single call stack, the queues and the event loop instead.
- Using await inside forEach, mutating arrays that should be copied, or comparing objects with === and expecting their contents to be checked.
- Jumping straight to React answers in a JavaScript round; show you understand the language itself before talking about any framework built on it.
- Writing code in silence during live coding; talk through your approach, edge cases such as empty input, and the time complexity of your solution.
JavaScript interview questions: Easy questions (freshers)
1. What is the difference between var, let and const?
var is function-scoped, can be redeclared and is initialised to undefined when hoisted. let and const are block-scoped, cannot be redeclared in the same scope and sit in the temporal dead zone until their declaration runs. const also stops reassignment of the binding, but it does not make the value immutable: you can still push to a const array or change a property of a const object. In modern code, use const by default, let when the value must change, and avoid var.
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(typeof b); // undefined (b only exists inside the block)
const list = [1];
list.push(2); // allowed: the array itself is mutable
console.log(list); // [ 1, 2 ] 2. What are the primitive data types in JavaScript?
There are seven primitives: string, number, bigint, boolean, undefined, symbol and null. Everything else, including arrays and functions, is an object. Primitives are immutable and compared by value, while objects are compared by reference. A well-known quirk is that typeof null returns "object"; this is a bug from the first version of the language that was never fixed for compatibility, so check for null with value === null instead.
console.log(typeof "hi"); // string
console.log(typeof 42); // number
console.log(typeof 42n); // bigint
console.log(typeof Symbol()); // symbol
console.log(typeof undefined); // undefined
console.log(typeof null); // object (legacy quirk)
console.log(typeof [] , typeof function () {}); // object function 3. What is the difference between == and ===?
=== checks strict equality: both the type and the value must match, with no conversion. == allows type coercion first, using rules that are hard to remember, so 0 == "" is true and null == undefined is true, but null == 0 is false. Most teams use === everywhere. The one common, deliberate use of == is value == null, which matches both null and undefined in a single check.
console.log(0 == ""); // true
console.log(0 === ""); // false
console.log("1" == 1); // true
console.log(null == undefined); // true
console.log(null == 0); // false
console.log(null === undefined); // false 4. What is the difference between null and undefined?
undefined means a value was never assigned: an uninitialised variable, a missing object property, a missing function argument, or a function with no return. null is an intentional "no value" that a programmer sets, for example to clear a selected item. typeof undefined is "undefined" while typeof null is "object". In JSON, null is kept but properties with undefined values are dropped, which matters when you send data to an API.
let city;
console.log(city); // undefined
const user = { name: "Asha", manager: null };
console.log(user.age); // undefined (property missing)
console.log(user.manager); // null (set on purpose)
console.log(JSON.stringify({ a: undefined, b: null })); // {"b":null} 5. What is hoisting in JavaScript?
Hoisting means declarations are processed before the code in a scope runs. var declarations are hoisted and initialised to undefined, so reading one early gives undefined instead of an error. Function declarations are hoisted with their body, so you can call them above where they are written. let, const and class are hoisted too, but reading them before the declaration throws a ReferenceError because they are in the temporal dead zone. Function expressions follow the rules of the variable they are assigned to.
console.log(x); // undefined
var x = 5;
sayHi(); // hi (the whole declaration is hoisted)
function sayHi() {
console.log("hi");
} 6. What is a closure?
A closure is a function together with the variables from the scope where it was created. The inner function keeps access to those variables even after the outer function has returned. Closures are how you get private state without classes, and they power callbacks, event handlers, debounce, memoisation and React hooks. The trade-off is memory: anything a live closure references cannot be garbage collected.
function makeCounter() {
let count = 0; // private to this counter
return () => ++count;
}
const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2
const other = makeCounter();
console.log(other()); // 1 (separate closure, separate count) 7. How are arrow functions different from regular functions?
Arrow functions are shorter and, more importantly, they do not have their own this, arguments, super or new.target; they use the values from the surrounding scope. They cannot be called with new and have no prototype property. That makes them ideal for callbacks inside methods, where you want the outer this. They are a poor fit for object methods that need this to be the object, and for constructors.
const timer = {
seconds: 0,
start() {
setTimeout(() => {
this.seconds++; // arrow uses start()'s this
console.log(this.seconds); // 1
}, 0);
},
};
timer.start(); 8. What are truthy and falsy values?
A falsy value becomes false when used in a boolean context such as an if statement. The falsy values are false, 0, -0, 0n, the empty string, null, undefined and NaN. Everything else is truthy, including "0", "false", an empty array and an empty object, which surprises many beginners. This is why if (items.length) is a common way to check for a non-empty array, but if (items) is not.
const values = [0, "", null, undefined, NaN, "0", [], {}];
console.log(values.map(Boolean));
// [ false, false, false, false, false, true, true, true ]
if ([].length) console.log("never runs: empty array has length 0"); 9. Explain map, filter and reduce with an example.
All three take a callback and do not change the original array. map transforms every element and returns a new array of the same length. filter returns a new array with only the elements for which the callback returns a truthy value. reduce walks the array and folds it into a single value, such as a sum or an object, using an accumulator. Always pass an initial value to reduce, otherwise it throws on an empty array.
const prices = [100, 250, 40];
const doubled = prices.map((p) => p * 2);
const costly = prices.filter((p) => p > 50);
const total = prices.reduce((sum, p) => sum + p, 0);
console.log(doubled); // [ 200, 500, 80 ]
console.log(costly); // [ 100, 250 ]
console.log(total); // 390 10. What is the difference between the spread and rest operators?
Both use three dots, but they do opposite jobs. Spread expands an iterable or object into individual items, for example to copy an array, merge objects or pass array items as function arguments. Rest collects several items into one array or object, for example extra function arguments or the remaining properties in destructuring. A quick rule: on the right side or in a call it is spread; in a parameter list or on the left side of destructuring it is rest.
function sum(...nums) { // rest: collects arguments
return nums.reduce((a, b) => a + b, 0);
}
const arr = [1, 2, 3];
console.log(sum(...arr)); // spread: 6
const user = { name: "Asha", city: "Pune", age: 26 };
const { age, ...rest } = user; // rest in destructuring
console.log({ ...rest, city: "Delhi" }); // { name: 'Asha', city: 'Delhi' } 11. What is destructuring?
Destructuring unpacks values from arrays or properties from objects into variables in one statement. Array destructuring works by position, so you can skip items with empty slots. Object destructuring works by property name and lets you rename a variable and give a default that applies only when the value is undefined. It is widely used for function parameters, React props and values returned from APIs.
const user = { name: "Ravi", age: 28 };
const { name, age: years, city = "Mumbai" } = user;
const [first, , third] = [10, 20, 30];
console.log(name, years, city); // Ravi 28 Mumbai
console.log(first, third); // 10 30
function greet({ name, role = "guest" }) {
return `${name} (${role})`;
}
console.log(greet(user)); // Ravi (guest) 12. What is a Promise?
A Promise is an object that represents the result of an asynchronous operation that may finish later. It starts as pending and settles once, either fulfilled with a value or rejected with a reason; after that it never changes. You react to it with then, catch and finally, or with await. Promises replaced deeply nested callbacks and make it easy to chain steps and handle errors in one place.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const check = (ok) =>
new Promise((resolve, reject) => (ok ? resolve("valid") : reject(new Error("invalid"))));
wait(100).then(() => console.log("100ms passed"));
check(false)
.then((v) => console.log(v))
.catch((err) => console.log("Error:", err.message)) // Error: invalid
.finally(() => console.log("check finished")); 13. How do async and await work?
An async function always returns a Promise. Inside it, await pauses that function until the awaited Promise settles, then gives back the fulfilled value or throws the rejection reason. Other code keeps running while it waits, so the thread is not blocked. Errors are handled with an ordinary try/catch, which reads like synchronous code. Remember that awaiting calls one after another runs them in sequence; use Promise.all when they are independent.
const fetchScore = () => Promise.resolve(42);
async function show() {
try {
const score = await fetchScore();
console.log("Score:", score);
} catch (err) {
console.error("Failed:", err.message);
}
}
show(); // Score: 42 14. What is event bubbling?
When an event fires on an element, it first runs the handlers on that target and then bubbles up through each ancestor to the document. That is why a click on a button also triggers a click listener on its parent div. Before bubbling there is a capture phase going down, which you can listen to with { capture: true }. Calling event.stopPropagation() stops the event from moving further, but use it sparingly because it can break analytics and delegated handlers.
document.body.innerHTML = '<div id="outer"><button id="btn">Pay</button></div>';
const outer = document.getElementById("outer");
const btn = document.getElementById("btn");
outer.addEventListener("click", () => console.log("outer"));
btn.addEventListener("click", () => console.log("button"));
btn.click(); // button, then outer 15. What is event delegation?
Event delegation means attaching one listener to a common parent instead of one listener per child. Because events bubble, the parent receives clicks from every child and uses event.target (often with closest()) to find which one was clicked. It uses less memory and, importantly, works for children added later, such as rows loaded by AJAX. Check that the matched element is inside the parent before acting on it.
document.body.innerHTML = '<ul id="list"><li>Apple</li><li>Mango</li></ul>';
const list = document.getElementById("list");
list.addEventListener("click", (e) => {
const li = e.target.closest("li");
if (li && list.contains(li)) console.log("Clicked:", li.textContent);
});
list.insertAdjacentHTML("beforeend", "<li>Guava</li>"); // added later
document.querySelector("li:last-child").click(); // Clicked: Guava 16. What is NaN, and how do you check for it correctly?
NaN means "not a number" and is the result of failed numeric operations such as Number("abc") or 0 / 0. Oddly, typeof NaN is "number", and NaN is the only value not equal to itself, so x === NaN is always false. The global isNaN() converts its argument to a number first, so isNaN("hello") is true. Number.isNaN() does no conversion and returns true only for the actual NaN value, so prefer it.
console.log(typeof NaN); // number
console.log(NaN === NaN); // false
console.log(isNaN("hello")); // true (converts to NaN first)
console.log(Number.isNaN("hello")); // false
console.log(Number.isNaN(0 / 0)); // true 17. What is the difference between a shallow copy and a deep copy?
A shallow copy creates a new top-level object, but nested objects are still shared by reference, so changing a nested value through the copy also changes the original. Spread syntax and Object.assign make shallow copies. A deep copy duplicates every level. The built-in structuredClone() makes deep copies and handles Dates, Maps, Sets and circular references, but it cannot copy functions or DOM nodes.
const a = { name: "Neha", address: { city: "Jaipur" } };
const shallow = { ...a };
const deep = structuredClone(a);
shallow.address.city = "Kota";
console.log(a.address.city); // Kota (shared nested object)
console.log(deep.address.city); // Jaipur 18. How do import and export work in ES modules?
ES modules let each file export values and other files import them. A file can have many named exports, imported with curly braces and the exact names (you can rename with as), and at most one default export, imported with any name you choose. Modules run in strict mode, have their own scope, and are evaluated only once no matter how many files import them. In browsers you load them with <script type="module">, and in Node with .mjs files or "type": "module" in package.json.
// math.js
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
export default function round2(n) { return Math.round(n * 100) / 100; }
// app.js
import round2, { area, PI as pi } from "./math.js";
console.log(round2(area(2)), pi); // 12.57 3.14159 19. What does the this keyword refer to?
this is decided by how a function is called, not where it is written. In a method call like user.greet(), this is the object before the dot. In a plain function call it is undefined in strict mode (and the global object in sloppy mode). With new it is the newly created object, and call, apply or bind set it explicitly. Arrow functions have no this of their own and use the surrounding one. A common bug is passing a method as a callback, which loses its object.
const user = {
name: "Kiran",
greet() {
return "Hi " + this.name;
},
};
console.log(user.greet()); // Hi Kiran
const loose = user.greet.bind(user); // fixes this for later calls
console.log(loose()); // Hi Kiran 20. What are optional chaining (?.) and nullish coalescing (??)?
Optional chaining stops and returns undefined if the value before ?. is null or undefined, instead of throwing "Cannot read properties of undefined". It also works for calls, fn?.(), and indexes, arr?.[0]. Nullish coalescing returns the right side only when the left side is null or undefined. That is the key difference from ||, which also replaces valid falsy values such as 0 or an empty string.
const order = { customer: { name: "Sana" }, discount: 0 };
console.log(order.customer?.address?.city); // undefined, no error
console.log(order.discount || 10); // 10 (0 wrongly replaced)
console.log(order.discount ?? 10); // 0
console.log(order.notify?.("sms")); // undefined, method missing JavaScript interview questions: Medium questions (1-3 years)
21. What does this loop print, and how do you fix it?
The var loop prints 3 three times. var creates one function-scoped i shared by every iteration, and by the time the timers run the loop has finished and i is 3. With let, each iteration gets a fresh binding, so each callback closes over its own value and prints 0, 1, 2. Before let existed, the fix was an IIFE or passing i as a setTimeout argument. Interviewers use this to check that you understand closures and block scope together.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}
// var: 3, var: 3, var: 3, then let: 0, let: 1, let: 2 22. Explain the event loop. What is the output of this code?
JavaScript runs one call stack. When the stack is empty, the event loop first drains the whole microtask queue (promise callbacks, queueMicrotask, await continuations), and only then takes the next macrotask such as a timer or I/O callback; browsers may render between macrotasks. So synchronous logs come first (1, 5), then microtasks in the order they were queued (3, 4), and the setTimeout callback last (2), even with a 0ms delay.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
queueMicrotask(() => console.log("4"));
console.log("5");
// 1, 5, 3, 4, 2 23. What is the difference between call, apply and bind?
All three set the value of this for a function. call invokes the function immediately with arguments listed one by one. apply also invokes immediately but takes the arguments as an array. bind does not call the function; it returns a new function with this (and optionally some leading arguments) fixed permanently, which is useful for callbacks and event handlers. A bound function's this cannot be changed again by call or apply.
function intro(city, role) {
return `${this.name} from ${city}, ${role}`;
}
const person = { name: "Arjun" };
console.log(intro.call(person, "Chennai", "Developer"));
console.log(intro.apply(person, ["Chennai", "Developer"]));
const fromChennai = intro.bind(person, "Chennai");
console.log(fromChennai("Tester")); // Arjun from Chennai, Tester 24. Write a debounce function and explain where you would use it.
Debounce delays a function until a set time has passed without another call. Every new call clears the previous timer and starts a new one, so only the last call in a burst runs. Typical uses are search-as-you-type (call the API after the user stops typing), auto-save, and window resize handlers. Keeping this and the arguments with apply means it also works for object methods. Production versions often add a cancel method and a leading-edge option.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((q) => console.log("search:", q), 300);
search("r");
search("re");
search("rea"); // only "search: rea" runs, 300ms after the last call 25. Write a throttle function. How is it different from debounce?
Throttle guarantees a function runs at most once per interval, however often it is called, while debounce waits for the calls to stop. Throttle suits continuous events where you want regular updates, such as scroll position, mouse move or infinite-scroll checks. Debounce suits events where only the final value matters, like a search box. This simple version drops calls inside the interval; a fuller version also schedules one trailing call so the last event is not lost.
function throttle(fn, interval) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
const onScroll = throttle(() => console.log("scroll handled"), 200);
onScroll();
onScroll(); // ignored: still inside the 200ms window 26. Compare Promise.all, Promise.allSettled, Promise.race and Promise.any.
Promise.all fulfils with an array of all results, in input order, and rejects as soon as any one rejects. allSettled always waits for every promise and gives a status object for each, which suits batch jobs where partial failure is fine. race settles with whichever promise settles first, success or failure, and is often used for timeouts. any fulfils with the first success and rejects with an AggregateError only if every promise rejects.
const ok = (v, ms) => new Promise((r) => setTimeout(() => r(v), ms));
const fail = (msg) => Promise.reject(new Error(msg));
Promise.all([ok(1, 10), fail("boom")]).catch((e) => console.log(e.message)); // boom
Promise.allSettled([ok("A", 50), fail("B")])
.then((r) => console.log(r.map((x) => x.status))); // [ 'fulfilled', 'rejected' ]
Promise.race([ok("fast", 10), ok("slow", 100)]).then(console.log); // fast
Promise.any([fail("x"), ok("first success", 20)]).then(console.log); // first success 27. How does prototypal inheritance work?
Every object has an internal link to another object, its prototype. When you read a property that the object does not have, JavaScript looks it up on the prototype, then that object's prototype, and so on until it reaches null. This is the prototype chain. Methods are usually shared this way instead of being copied onto every object. Writing a property always creates it on the object itself, which shadows the prototype's version.
const animal = {
speak() { return `${this.name} makes a sound`; },
};
const dog = Object.create(animal);
dog.name = "Tommy";
console.log(dog.speak()); // Tommy makes a sound
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.hasOwn(dog, "speak")); // false (inherited)
console.log(Object.hasOwn(dog, "name")); // true 28. Are JavaScript classes just syntactic sugar over prototypes?
Mostly, yes: a class still creates a constructor function, and its methods live on that function's prototype. But classes add real behaviour that the old pattern did not have. Class bodies are always in strict mode, a class cannot be called without new, declarations sit in the temporal dead zone, and true private fields with # are enforced by the engine rather than by naming convention. extends and super also set up the prototype chain for you.
class Account {
#balance = 0;
constructor(owner) { this.owner = owner; }
deposit(amount) { this.#balance += amount; return this.#balance; }
}
class Savings extends Account {
deposit(amount) { return super.deposit(amount + 5); } // Rs 5 bonus
}
const s = new Savings("Meera");
console.log(s.deposit(100)); // 105
console.log(typeof Account); // function
console.log(Object.hasOwn(Account.prototype, "deposit")); // true 29. Why does this inside a nested callback lose its value, and how do you fix it?
A regular function passed as a callback is called later as a plain function, not as a method, so it gets its own this: undefined in strict mode or the global object otherwise. It does not inherit this from the method around it. The usual fix is an arrow function, which takes this from the enclosing method. Older code used bind(this) or saved const self = this. Many array methods also accept a thisArg, but arrows are clearer.
const team = {
name: "Platform",
members: ["Ana", "Raj"],
list() {
return this.members.map((m) => `${m} (${this.name})`);
},
listBroken() {
return this.members.map(function (m) { return `${m} (${this?.name})`; });
},
};
console.log(team.list()); // [ 'Ana (Platform)', 'Raj (Platform)' ]
console.log(team.listBroken()); // [ 'Ana (undefined)', 'Raj (undefined)' ] 30. Write a curry function. Where is currying useful?
Currying turns a function that takes several arguments into a chain of functions that each take some of them, until enough arguments are collected to call the original. This version uses fn.length, the number of declared parameters, to decide when to call it, and allows several arguments per step. Currying helps build specialised functions from general ones, such as a logger with a fixed level or a tax calculator with a fixed rate. Note that fn.length ignores rest parameters and parameters with defaults.
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length
? fn.apply(this, args)
: (...more) => curried.apply(this, [...args, ...more]);
};
}
const add3 = curry((a, b, c) => a + b + c);
console.log(add3(1)(2)(3)); // 6
console.log(add3(1, 2)(3)); // 6 31. When would you use a Map or Set instead of a plain object or array?
Use a Map when keys are not plain strings, for example objects or numbers you do not want converted to strings, when you add and remove keys often, or when you need insertion order and a size property. A plain object is fine for fixed, known keys and for JSON. Use a Set when you need unique values and fast has() checks instead of scanning an array with includes(). WeakMap and WeakSet hold object keys weakly, so they do not stop garbage collection.
const visits = new Map();
const user = { id: 7 };
visits.set(user, 3); // an object as a key
console.log(visits.get(user)); // 3
console.log(visits.size); // 1
const tags = new Set(["js", "react", "js"]);
console.log(tags.has("react"), tags.size); // true 2
console.log([...new Set([1, 2, 2, 3])]); // [ 1, 2, 3 ] 32. What is the difference between const and Object.freeze?
const protects the variable binding: you cannot point the name at a different value, but the object it holds can still be changed. Object.freeze protects the object: its properties cannot be added, removed or changed. Freezing is shallow, so nested objects stay mutable unless you freeze them too. In sloppy mode a write to a frozen object fails silently; in strict mode, including ES modules and classes, it throws a TypeError.
const config = Object.freeze({ env: "prod", db: { host: "a" } });
config.env = "dev"; // ignored here (TypeError in strict mode)
config.db.host = "b"; // allowed: freeze is shallow
console.log(config.env, config.db.host); // prod b
console.log(Object.isFrozen(config), Object.isFrozen(config.db)); // true false 33. Which newer array methods (ES2022 and ES2023) do you use, and why?
at() reads from the end with negative indexes. ES2023 added findLast and findLastIndex, and non-mutating copies of mutating methods: toSorted, toReversed, toSpliced and with(index, value). These return a new array and leave the original unchanged, which is exactly what React state and Redux reducers need. Also remember that sort() without a comparator sorts as strings, so [10, 9, 1].sort() gives [1, 10, 9]; pass (a, b) => a - b for numbers.
const scores = [40, 10, 30];
const sorted = scores.toSorted((a, b) => a - b);
console.log(sorted, scores); // [ 10, 30, 40 ] [ 40, 10, 30 ]
console.log(scores.with(1, 99)); // [ 40, 99, 30 ]
console.log(scores.findLast((n) => n > 20)); // 30
console.log(scores.at(-1)); // 30
console.log([10, 9, 1].sort()); // [ 1, 10, 9 ] 34. Why does await not work as expected inside forEach?
forEach calls the callback for each item and ignores what it returns. An async callback returns a promise, so forEach starts every call and moves on without waiting, and the surrounding function cannot await the results or catch their errors. Use for...of with await when the steps must run one after another, or map the items to promises and await Promise.all when they can run in parallel. Choose based on order needs and on the load you put on the API.
const save = (id) => new Promise((r) => setTimeout(() => r(id), 10));
async function run(ids) {
ids.forEach(async (id) => { await save(id); });
console.log("forEach returned immediately");
for (const id of ids) await save(id);
console.log("for...of finished, one by one");
await Promise.all(ids.map(save));
console.log("Promise.all finished, in parallel");
}
run([1, 2, 3]); 35. What are the differences between CommonJS and ES modules?
CommonJS uses require() and module.exports; it loads synchronously at runtime, so you can require conditionally, and it is the older Node format. ES modules use static import and export, which tools can analyse ahead of time for tree shaking. They are always strict, support top-level await, and their imports are live read-only bindings to the exporter's variables. Node treats .mjs files, or .js files under "type": "module", as ESM. Recent Node versions can also require() an ES module as long as it has no top-level await.
36. How do for...in, Object.keys and Object.entries differ?
for...in loops over enumerable string keys, including keys inherited through the prototype chain, which is why older code wrapped it in a hasOwnProperty check. Object.keys returns only the object's own enumerable string keys as an array, and Object.entries returns own [key, value] pairs, which pair nicely with destructuring. None of them include symbol keys. Use for...of with an array, not for...in, because for...in gives string indexes and any extra enumerable properties.
const base = { inherited: true };
const obj = Object.create(base);
obj.own = 1;
for (const k in obj) console.log("for...in:", k); // own, inherited
console.log(Object.keys(obj)); // [ 'own' ]
for (const [key, value] of Object.entries(obj)) {
console.log(key, value); // own 1
} 37. Predict the output of these type coercion expressions.
The + operator concatenates if either side is a string after conversion to a primitive, so "5" + 2 is "52". Other arithmetic operators always convert to numbers, so "5" - 2 is 3. Arrays convert to primitives through their string form: [] becomes "", so [] + [] is an empty string, and {} becomes "[object Object]". true becomes 1 in arithmetic. With ==, [10] becomes "10" and then the number 10. Being able to explain these rules matters more than memorising results.
console.log("5" + 2); // 52 (string)
console.log("5" - 2); // 3
console.log([] + []); // "" (empty string, prints a blank line)
console.log([] + {}); // [object Object]
console.log(true + 1); // 2
console.log([10] == 10); // true 38. What is the temporal dead zone?
The temporal dead zone is the period between entering a scope and the line where a let, const or class declaration runs. The name is already reserved in that scope, so any access during this time throws a ReferenceError instead of reading an outer variable or getting undefined. In the example, the inner let x shadows the outer x for the whole function body, so the console.log fails. The TDZ turns silent bugs from var hoisting into clear errors.
let x = "outer";
function show() {
console.log(x); // throws: the inner x is in the TDZ
let x = "inner";
}
try {
show();
} catch (e) {
console.log(e.name); // ReferenceError
} 39. What are the differences between localStorage, sessionStorage and cookies?
localStorage keeps string data per origin with no expiry until it is cleared, and it is shared across tabs. sessionStorage is per tab and is cleared when the tab closes. Both hold roughly 5 MB, are synchronous and are never sent to the server automatically. Cookies are small (about 4 KB), are sent with every matching request, and can have an expiry. For login sessions, use HttpOnly, Secure, SameSite cookies, because JavaScript cannot read HttpOnly cookies, which limits token theft through XSS. Never keep secrets in localStorage.
40. Write a memoize function using a closure.
Memoisation caches results by input, so repeated calls with the same argument return instantly instead of recomputing. The cache lives in a closure, so it is private to the returned function and persists between calls. Using a Map is safer than an object because keys keep their type. Memoise only pure functions, because a cached result is wrong if the output depends on anything other than the input. Also think about memory: an unbounded cache grows forever, so long-running apps often use an LRU limit.
function memoize(fn) {
const cache = new Map();
return (n) => {
if (cache.has(n)) return cache.get(n);
const result = fn(n);
cache.set(n, result);
return result;
};
}
const square = memoize((n) => { console.log("computing", n); return n * n; });
console.log(square(4)); // computing 4, then 16
console.log(square(4)); // 16 (from the cache) JavaScript interview questions: Hard questions (experienced)
41. What is the output order of this async/await code, and why?
The order is start, a1, b, end, a2, then, timeout. Calling a() runs synchronously until the first await, so a1 and b print immediately. b() returns an already-resolved promise, so the rest of a() is queued as a microtask at that moment. The .then callback is queued after it, so a2 comes before then. end prints before both because microtasks only run once the current script finishes. The timer is a macrotask, so it runs last.
async function a() {
console.log("a1");
await b();
console.log("a2");
}
async function b() { console.log("b"); }
console.log("start");
setTimeout(() => console.log("timeout"), 0);
a();
Promise.resolve().then(() => console.log("then"));
console.log("end");
// start, a1, b, end, a2, then, timeout 42. Implement Promise.all yourself.
Return a new promise. Wrap every item with Promise.resolve so plain values and thenables both work, store each result at its own index so the output order matches the input order rather than completion order, and count completions to know when to resolve. Pass reject directly so the first failure rejects the whole thing; later settlements are ignored because a promise settles only once. Handle the empty array, which must resolve immediately with []. The real method accepts any iterable, which this version would need to copy into an array first.
function promiseAll(items) {
return new Promise((resolve, reject) => {
const results = new Array(items.length);
let done = 0;
if (items.length === 0) return resolve(results);
items.forEach((item, i) => {
Promise.resolve(item).then((value) => {
results[i] = value;
if (++done === items.length) resolve(results);
}, reject);
});
});
}
promiseAll([1, Promise.resolve(2), 3]).then(console.log); // [ 1, 2, 3 ] 43. Can a page freeze even if all your code is asynchronous?
Yes. The event loop drains the entire microtask queue before it handles any timer, input event or render. If microtasks keep scheduling more microtasks, for example a promise chain that recursively calls itself, timers and rendering wait until the chain ends. In the example the timer fires only after a million microtasks. Long synchronous loops inside async functions block too. For heavy work, break it into chunks with setTimeout or scheduler.yield() where supported, or move it to a Web Worker.
let n = 0;
function spin() {
if (++n < 1e6) Promise.resolve().then(spin);
}
setTimeout(() => console.log("timer ran after", n, "microtasks"), 0);
spin();
// timer ran after 1000000 microtasks 44. What causes memory leaks in front-end JavaScript, and how do you find them?
Common causes are listeners and intervals that are never removed when a component or page section goes away, detached DOM nodes still referenced from JavaScript, caches and global Maps that only grow, and closures that hold large objects longer than needed. To find them, repeat the suspected action several times and compare heap snapshots in Chrome DevTools, looking for growing counts and detached elements. Fixes include cleanup functions, WeakMap for per-object data, bounded caches and AbortController to remove many listeners at once.
const onResize = () => console.log("resize");
const onKey = (e) => console.log("key", e.key);
const controller = new AbortController();
window.addEventListener("resize", onResize, { signal: controller.signal });
document.addEventListener("keydown", onKey, { signal: controller.signal });
// On teardown, one call removes both listeners:
controller.abort(); 45. What are the limits of structuredClone and JSON.parse(JSON.stringify()) for copying?
JSON round-tripping is lossy: Dates become strings, undefined properties and functions are dropped, Map and Set become empty objects, NaN and Infinity become null, BigInt throws and circular references throw. structuredClone is much better: it keeps Dates, Maps, Sets, typed arrays, BigInt and cycles. But it throws a DataCloneError for functions and DOM nodes, and it does not keep class prototypes, so a class instance comes back as a plain object without its methods. For class instances, write an explicit clone method.
const src = { when: new Date(0), tags: new Set(["a"]), big: 10n, fn() {} };
try { structuredClone(src); } catch (e) { console.log(e.name); } // DataCloneError
delete src.fn;
const c = structuredClone(src);
console.log(c.when instanceof Date, c.tags.has("a"), c.big); // true true 10n
console.log(JSON.stringify({ when: new Date(0), u: undefined }));
// {"when":"1970-01-01T00:00:00.000Z"} 46. What does the new keyword do? Implement it as a function.
new does four things: creates an empty object, links its prototype to the constructor's prototype property, runs the constructor with this bound to the new object, and returns that object, unless the constructor explicitly returns another object or function, in which case that wins. Returning a primitive is ignored. This implementation works for constructor functions; class constructors refuse to be called without new, so a real-world version would use Reflect.construct.
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const result = Ctor.apply(obj, args);
const isObject = result !== null && (typeof result === "object" || typeof result === "function");
return isObject ? result : obj;
}
function Person(name) { this.name = name; }
Person.prototype.hi = function () { return "Hi " + this.name; };
const p = myNew(Person, "Isha");
console.log(p.hi(), p instanceof Person); // Hi Isha true 47. Write a polyfill for Function.prototype.bind. What does the simple version miss?
The core idea is a closure: save the original function, the target this and any preset arguments, and return a function that calls the original with apply, joining preset and later arguments. The simple version misses two details of the native bind. If the bound function is called with new, the native version ignores the bound this and constructs a new instance; and native bound functions have a name like "bound greet" and a length reduced by the preset arguments. Mentioning these gaps shows depth.
Function.prototype.myBind = function (ctx, ...preset) {
const fn = this;
return function (...later) {
return fn.apply(ctx, [...preset, ...later]);
};
};
function greet(greeting, mark) { return `${greeting}, ${this.name}${mark}`; }
const hello = greet.myBind({ name: "Tara" }, "Hello");
console.log(hello("!")); // Hello, Tara!
console.log(greet.bind({}).name); // bound greet (native) 48. What is a Proxy, and when would you use one?
A Proxy wraps an object and lets you intercept basic operations such as get, set, has and deleteProperty through handler functions called traps. Inside a trap you usually forward to Reflect so the default behaviour still happens. Real uses include validation, change tracking for reactive state (Vue 3 and MobX are built on proxies), default values, logging and access control. The costs are slower property access and identity issues: the proxy is a different object from its target.
const user = new Proxy({ age: 25 }, {
set(target, key, value) {
if (key === "age" && (!Number.isInteger(value) || value < 0)) {
throw new TypeError("age must be a non-negative integer");
}
return Reflect.set(target, key, value);
},
});
user.age = 30;
try { user.age = -1; } catch (e) { console.log(e.message); }
console.log(user.age); // 30 49. How is Object.is different from ===?
They agree on everything except two cases. NaN === NaN is false, but Object.is(NaN, NaN) is true. 0 === -0 is true, but Object.is(0, -0) is false. Object.is is the SameValue algorithm, and React uses it to decide whether state changed. Array methods differ too: includes uses SameValueZero, so it finds NaN, while indexOf uses strict equality and does not. -0 can matter in maths, for example 1 / -0 is -Infinity.
console.log(NaN === NaN, Object.is(NaN, NaN)); // false true
console.log(0 === -0, Object.is(0, -0)); // true false
console.log([NaN].includes(NaN), [NaN].indexOf(NaN)); // true -1
console.log(1 / -0); // -Infinity 50. Why is 0.1 + 0.2 not 0.3, and how should you handle money in JavaScript?
Numbers are 64-bit binary floating point, and decimals like 0.1 have no exact binary form, so small errors appear: 0.1 + 0.2 gives 0.30000000000000004. For money, store and calculate in the smallest unit, paise, as integers, and convert only for display. Integers are exact up to Number.MAX_SAFE_INTEGER; beyond that use BigInt. Round explicitly when applying percentages such as GST. For display, Intl.NumberFormat with en-IN gives Indian grouping and the rupee sign.
console.log(0.1 + 0.2); // 0.30000000000000004
const pricePaise = 49999; // Rs 499.99
const gst = Math.round(pricePaise * 18 / 100);
const total = pricePaise + gst;
console.log(total); // 58999 paise
const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
console.log(inr.format(total / 100)); // ₹589.99
console.log(inr.format(1234567.5)); // ₹12,34,567.50 51. How do ES modules handle circular imports, and what are live bindings?
An ES module import is a live, read-only binding to the exporting module's variable, not a copy, so if the exporter later changes the value, importers see the new value. With CommonJS, destructuring require() copies values at that moment. In a cycle, ESM links all modules first and then evaluates them depth-first, so one module may run while the other has not finished. Hoisted function declarations are usable early, but reading an exported let, const or class before its module runs throws a ReferenceError. The safe fix is to move shared code into a third module.
52. How do you prevent stale responses in a search-as-you-type feature?
If the user types quickly, an older, slower request can come back after a newer one and overwrite the correct results. The cleanest fix is to cancel the previous request with AbortController before starting a new one, which also saves bandwidth. A cancelled fetch rejects with an AbortError, which you treat as normal and ignore. Combine this with debouncing to cut request volume. Without cancellation, you can tag each request with an id and ignore any response that is not the latest.
let controller;
async function search(q) {
controller?.abort(); // cancel the previous request
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`,
{ signal: controller.signal });
return await res.json();
} catch (err) {
if (err.name === "AbortError") return null; // superseded, ignore
throw err;
}
} 53. Write a retry helper with exponential backoff.
Retry the call when it fails, waiting longer each time: base, 2 x base, 4 x base, and so on, so a struggling server gets breathing room. Add random jitter so many clients do not retry at the same moment. Stop after a fixed number of attempts and rethrow the last error. In production, retry only errors that can succeed later, such as timeouts, 429 and 5xx responses, never 400 or 401, and make sure the operation is safe to repeat, for example a payment request with an idempotency key.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function retry(fn, attempts = 3, baseMs = 100) {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err) {
if (i >= attempts - 1) throw err;
await sleep(baseMs * 2 ** i + Math.random() * 50); // backoff + jitter
}
}
}
let calls = 0;
const flaky = async () => { if (++calls < 3) throw new Error("503"); return "ok"; };
retry(flaky).then((r) => console.log(r, "after", calls, "calls")); // ok after 3 calls 54. What are generators, and when are they useful?
A generator function, written function*, returns an iterator. Each yield pauses the function and hands out a value; calling next() resumes it from the same point with its local state intact. Because values are produced only on demand, generators can describe infinite or very large sequences without building an array, and they plug into for...of and spread. Uses include ID sequences, lazy pipelines such as take and filter, tree traversal, and custom iterables via Symbol.iterator.
function* idGenerator(prefix) {
let n = 1;
while (true) yield `${prefix}-${n++}`;
}
const ids = idGenerator("EMP");
console.log(ids.next().value, ids.next().value); // EMP-1 EMP-2
function* take(iter, k) {
for (const x of iter) { if (k-- <= 0) return; yield x; }
}
console.log([...take(idGenerator("INV"), 3)]); // [ 'INV-1', 'INV-2', 'INV-3' ] 55. How would you read every page of a paginated API lazily?
Use an async generator. It fetches one page, yields its items one by one, and only requests the next page when the consumer asks for more items. The caller uses for await...of and can break early, which stops further requests. This keeps memory flat for large exports and separates the paging logic from the code that processes items. Add a page cap or timeout in real code so a buggy API that always returns a next page cannot loop forever.
async function fetchPage(page) { // stand-in for a real API call
return { items: [page * 10, page * 10 + 1], next: page < 3 ? page + 1 : null };
}
async function* allItems() {
let page = 1;
while (page) {
const res = await fetchPage(page);
yield* res.items;
page = res.next;
}
}
(async () => {
for await (const item of allItems()) console.log(item); // 10 11 20 21 30 31
})(); 56. In Node.js, in what order do process.nextTick, promises, setTimeout and setImmediate run?
In a CommonJS script, process.nextTick callbacks run first, then promise microtasks, both before the event loop moves on. setTimeout(fn, 0) and setImmediate then race: when called from the main module their order is not guaranteed, because it depends on how quickly the loop starts. Inside an I/O callback, setImmediate always runs before the timer. In an ES module the order of nextTick and promises can flip, because module code itself runs inside a microtask. Prefer queueMicrotask or promises over nextTick in new code.
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
// CommonJS: nextTick, promise, then timeout and immediate in either order 57. Implement an LRU cache in JavaScript.
A Map remembers insertion order, which makes an LRU cache short. On get, delete the key and insert it again, which moves it to the newest end. On set, do the same, then if the size exceeds the limit, delete the oldest key, which is the first key returned by map.keys(). Both operations are O(1) on average. In interviews, mention that the classic language-neutral answer is a hash map plus a doubly linked list, and that Map ordering is what lets JavaScript skip the list.
class LRUCache {
constructor(limit) { this.limit = limit; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return undefined;
const value = this.map.get(key);
this.set(key, value); // re-insert as most recently used
return value;
}
set(key, value) {
this.map.delete(key); // so an update also counts as recent use
this.map.set(key, value);
if (this.map.size > this.limit) this.map.delete(this.map.keys().next().value);
}
} 58. What are the traps in Array.prototype.sort, and how do you sort names correctly?
Without a comparator, sort converts elements to strings and compares UTF-16 code units, so numbers sort as text and uppercase letters come before lowercase. Always pass a comparator: (a, b) => a - b for numbers. sort mutates the array in place; use toSorted for a copy. Since ES2019 the sort is guaranteed stable, so equal items keep their original order, which makes multi-key sorting reliable. For people's names, use Intl.Collator or localeCompare so case and accents are handled properly.
console.log([1, 10, 2, 21].sort()); // [ 1, 10, 2, 21 ]
console.log([1, 10, 2, 21].sort((a, b) => a - b)); // [ 1, 2, 10, 21 ]
console.log(["zoya", "Amit", "ankit"].sort()); // [ 'Amit', 'ankit', 'zoya' ]
console.log(["b", "B", "a", "A"].sort()); // [ 'A', 'B', 'a', 'b' ]
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(["b", "B", "a", "A"].sort(collator.compare)); // [ 'a', 'A', 'b', 'B' ]
const staff = [{ n: "Ravi", d: 2 }, { n: "Anu", d: 1 }, { n: "Om", d: 2 }];
console.log(staff.toSorted((a, b) => a.d - b.d).map((s) => s.n)); // [ 'Anu', 'Ravi', 'Om' ] 59. Design a small event emitter. What pitfalls should it handle?
Keep a Map from event name to a Set of handlers, so the same handler is not added twice and removal is cheap. Make on() return an unsubscribe function, which callers can keep and call during cleanup to avoid leaks. In emit(), loop over a copy of the handlers, because a handler may unsubscribe itself or add new handlers while the loop runs. Private # fields stop outside code from tampering with the handler map. Also decide how one failing handler should affect the others.
class Emitter {
#handlers = new Map();
on(event, fn) {
if (!this.#handlers.has(event)) this.#handlers.set(event, new Set());
this.#handlers.get(event).add(fn);
return () => this.#handlers.get(event).delete(fn); // unsubscribe
}
emit(event, ...args) {
for (const fn of [...(this.#handlers.get(event) ?? [])]) fn(...args);
}
}
const bus = new Emitter();
const off = bus.on("login", (u) => console.log("welcome", u));
bus.emit("login", "Priya"); off(); bus.emit("login", "Priya"); // logs once 60. What is layout thrashing, and how do you avoid it?
Layout thrashing happens when code alternates between writing styles and reading layout values such as offsetWidth or getBoundingClientRect in a loop. Each read after a write forces the browser to recalculate layout synchronously, so a loop over hundreds of elements can take hundreds of layouts instead of one. The fix is to batch: do all reads first, then all writes, ideally inside requestAnimationFrame. For animations, prefer transform and opacity, which avoid layout entirely, and use DevTools Performance to spot "forced reflow" warnings.
const boxes = document.querySelectorAll(".box");
// Bad: a read then a write on each pass forces a layout every time
// boxes.forEach((b) => { b.style.height = b.offsetWidth / 2 + "px"; });
// Good: read everything first, then write in the next frame
const widths = [...boxes].map((b) => b.offsetWidth);
requestAnimationFrame(() => {
boxes.forEach((b, i) => { b.style.height = widths[i] / 2 + "px"; });
}); Ready to apply?
There are 1,111 open jobs asking for JavaScript on CoreHRx right now. Create a free profile once and apply with it.