JavaScript Interview Questions and Answers
This page covers the JavaScript questions asked most often in developer interviews, from types, scope and closures to promises, the event loop, DOM events and output-prediction puzzles. Questions are grouped by level and ordered from most to least common, so start at your level and make sure you can explain each answer and predict each output without running the code.
JavaScript interview questions: fresher level
What are the different data types in JavaScript?
JavaScript has eight data types: seven primitives and one non-primitive type, object. The primitives are string, number, bigint, boolean, undefined, null and symbol. Arrays, functions, dates and plain objects all fall under object.
Primitives are immutable and are copied by value. Objects are stored by reference, so two variables can point to the same object and a change through one is visible through the other. Use typeof to check a type, but remember its long-standing quirk: typeof null returns "object".
typeof 42; // "number"
typeof 10n; // "bigint"
typeof "hi"; // "string"
typeof undefined; // "undefined"
typeof null; // "object" (historical quirk)
typeof Symbol("id"); // "symbol"
typeof []; // "object"
typeof function () {}; // "function" Interview tip: Common follow-up: how do you check for an array? Use Array.isArray(value), because typeof returns "object" for arrays.
What is the difference between var, let and const?
var is function-scoped and can be redeclared, while let and const are block-scoped and cannot be redeclared in the same scope. const additionally cannot be reassigned after it is declared.
varis hoisted and initialised toundefined;letandconstare hoisted but stay in the temporal dead zone, so using them before the declaration throws a ReferenceError.- A top-level
varin a classic script becomes a property ofwindow;letandconstdo not. constmust be initialised when declared, and it does not make objects immutable: you can still change their properties.- Default to
const, useletwhen the value must change, and avoidvarin new code.
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not definedconst user = { name: "Riya" };
user.name = "Aman"; // allowed: the object itself is mutated
user = {}; // TypeError: Assignment to constant variable.
What is the difference between == and === in JavaScript?
=== (strict equality) compares value and type without any conversion, while == (loose equality) first converts the operands to a common type and then compares them.
The conversion rules for == are hard to remember and give surprising results, so most style guides recommend === everywhere. One accepted exception is value == null, which is true only when the value is null or undefined.
5 == "5"; // true (string converted to number)
5 === "5"; // false (different types)
0 == false; // true
"" == 0; // true
null == undefined; // true
null === undefined; // false
null == 0; // false
NaN == NaN; // false Interview tip: Interviewers sometimes follow up with Object.is, which behaves like === except that Object.is(NaN, NaN) is true and Object.is(0, -0) is false.
What is the difference between null and undefined?
undefined means a value has not been assigned, while null is an intentional "no value" that a programmer sets on purpose.
- JavaScript itself gives you
undefined: for a declared but unassigned variable, a missing object property, a missing function argument, or a function with noreturn. nullis never set automatically; you assign it to say "this is empty", for example when clearing a selected item.typeof undefinedis"undefined", buttypeof nullis"object".null == undefinedis true, butnull === undefinedis false.- In arithmetic,
nullbecomes0whileundefinedbecomesNaN.
let x;
console.log(x); // undefined
console.log({}.name); // undefined
console.log(null + 1); // 1
console.log(undefined + 1); // NaN
What is hoisting in JavaScript?
Hoisting is JavaScript's behaviour of processing declarations before any code in a scope runs, so some variables and functions can be used before the line where they are declared.
Function declarations are hoisted completely, so you can call them before they appear. var declarations are hoisted and initialised to undefined, but the assignment stays where it is. let, const and class are hoisted too, but they remain uninitialised in the temporal dead zone until their declaration runs. Function expressions and arrow functions follow the rules of the variable they are stored in.
console.log(a); // undefined
var a = 10;
greet(); // Hello
function greet() { console.log("Hello"); }
sayHi(); // TypeError: sayHi is not a function
var sayHi = () => console.log("Hi");
What are truthy and falsy values in JavaScript?
A falsy value is one that becomes false when converted to a boolean, for example inside an if condition; every other value is truthy.
The falsy values you need to know are false, 0, -0, 0n, "" (empty string), null, undefined and NaN. Everything else is truthy, including several values that surprise beginners: "0", "false", " " (a space), [], {} and every function.
if ([]) console.log("empty array is truthy"); // prints
if ("0") console.log("'0' is truthy"); // prints
console.log(Boolean("")); // false
console.log(!!NaN); // false
const count = 0;
console.log(count || 10); // 10 (0 is falsy)
console.log(count ?? 10); // 0 (?? only skips null and undefined) Interview tip: The || versus ?? difference for values like 0 and "" is a favourite follow-up, especially for default values in forms and API responses.
What is the difference between arrow functions and regular functions?
Arrow functions are a shorter syntax for functions, but the important difference is that they do not have their own this; they use the this of the scope where they are defined.
- No own
this, so they are handy for callbacks inside methods, but a poor choice for object methods themselves. - No own
argumentsobject; use rest parameters (...args) instead. - They cannot be used as constructors with
newand have noprototypeproperty. - A single-expression body returns its value automatically (implicit return).
const user = {
name: "Riya",
friends: ["Aman", "Neha"],
show() {
this.friends.forEach((f) => console.log(`${this.name} knows ${f}`));
},
};
user.show();
// Riya knows Aman
// Riya knows NehaWith a regular function callback inside forEach, this would not be user, so this.name would not give "Riya".
What is the difference between the spread and rest operators?
Both use the ... syntax, but spread expands an array, string or object into individual elements, while rest collects several elements into a single array or object.
Spread appears in function calls, array literals and object literals. Rest appears in function parameters and on the left side of destructuring, and it must always be the last item.
const nums = [1, 2, 3];
console.log(Math.max(...nums)); // 3
const more = [...nums, 4]; // [1, 2, 3, 4]
const base = { a: 1, b: 2 };
const copy = { ...base, b: 5 }; // { a: 1, b: 5 }
function sum(...values) {
return values.reduce((total, v) => total + v, 0);
}
console.log(sum(1, 2, 3)); // 6
const [first, ...others] = nums; // first = 1, others = [2, 3]
What is destructuring in JavaScript?
Destructuring is syntax that unpacks values from arrays, or properties from objects, into separate variables in a single statement.
Array destructuring works by position, while object destructuring works by property name. You can set default values, rename variables, skip array items, and destructure directly in function parameters, which is very common with React props and API responses. Defaults apply only when the value is undefined, not when it is null.
const [a, , c = 10] = [1, 2];
console.log(a, c); // 1 10
const user = { name: "Riya", city: "Pune" };
const { name: fullName, age = 21 } = user;
console.log(fullName, age); // Riya 21
let x = 1, y = 2;
[x, y] = [y, x]; // swap without a temp variable
console.log(x, y); // 2 1
const greet = ({ name }) => `Hi ${name}`;
What is the difference between map, filter, reduce and forEach?
map transforms every element and returns a new array of the same length, filter returns a new array with only the elements that pass a test, and reduce combines all elements into a single value. forEach simply runs a function for each element and returns undefined.
None of them changes the original array unless your callback mutates it. Use map when you need a transformed array instead of forEach with push. reduce takes an accumulator and an optional initial value; always pass the initial value, because calling reduce on an empty array without one throws a TypeError.
const prices = [100, 250, 400];
prices.map((p) => p * 2); // [200, 500, 800]
prices.filter((p) => p > 200); // [250, 400]
prices.reduce((sum, p) => sum + p, 0); // 750
prices.forEach((p) => p * 2); // undefined
What is the difference between slice and splice?
slice returns a shallow copy of part of an array without changing the original, while splice changes the original array by removing, replacing or inserting elements, and returns the removed elements.
slice(start, end) excludes the element at end and accepts negative indexes that count from the end. splice(start, deleteCount, ...items) works in place. Strings also have slice, but not splice, because strings are immutable.
const arr = [10, 20, 30, 40, 50];
console.log(arr.slice(1, 3)); // [20, 30]
console.log(arr.slice(-2)); // [40, 50]
console.log(arr); // [10, 20, 30, 40, 50] (unchanged)
const removed = arr.splice(1, 2, 99);
console.log(removed); // [20, 30]
console.log(arr); // [10, 99, 40, 50]
What is scope and the scope chain in JavaScript?
Scope decides where a variable can be accessed. JavaScript has global scope, function scope and block scope, and each ES module also has its own module scope.
- Global scope: declared outside any function or block, visible everywhere.
- Function scope: variables declared inside a function, including
var, are visible only inside it. - Block scope:
letandconstinside{ }are visible only in that block.
Scope is lexical: a function can see variables from the place where it was written, not from where it is called. When a variable is used, the engine looks in the current scope, then the enclosing one, and so on up to the global scope. This lookup path is the scope chain; if the name is not found anywhere, a ReferenceError is thrown.
const app = "Shop";
function outer() {
const user = "Riya";
function inner() {
console.log(app, user); // Shop Riya (found via the scope chain)
}
inner();
}
outer();
What is NaN, and how do you check for it?
NaN stands for "Not a Number" and is the result of a numeric operation that has no meaningful numeric answer, such as 0 / 0 or Number("abc"). Oddly, typeof NaN is "number".
NaN is the only JavaScript value that is not equal to itself, so NaN === NaN is false and you cannot test for it with ===. Use Number.isNaN. The older global isNaN converts its argument to a number first, so it also returns true for non-numeric strings like "hello".
console.log(typeof NaN); // number
console.log(NaN === NaN); // false
console.log(isNaN("hello")); // true (converted to NaN first)
console.log(Number.isNaN("hello")); // false
console.log(Number.isNaN(0 / 0)); // true
console.log(parseInt("42px")); // 42
console.log(Number("42px")); // NaN
What is a callback function, and what is callback hell?
A callback is a function passed as an argument to another function, which calls it at the right time.
Callbacks are used for synchronous work, like the function you give to map, and for asynchronous work, like setTimeout, event listeners and older Node.js APIs. When several async steps depend on each other, the callbacks nest inside one another, producing deeply indented code with error handling repeated at every level. This is called callback hell or the pyramid of doom. Promises and async/await were introduced largely to solve it.
function fetchUser(id, callback) {
setTimeout(() => callback({ id, name: "Riya" }), 1000);
}
fetchUser(1, (user) => {
console.log(user.name); // Riya (after about 1 second)
});
What is the difference between localStorage, sessionStorage and cookies?
localStorage keeps data with no expiry until it is cleared, sessionStorage keeps data only for the current tab session, and cookies are small pieces of data that the browser sends to the server with every matching HTTP request.
- Size: web storage typically allows a few megabytes per origin, depending on the browser; a single cookie is limited to about 4 KB.
localStorageis shared by all tabs of the same origin;sessionStorageis separate for each tab and is cleared when the tab is closed.- Cookies can have an expiry date, and the server can mark them
HttpOnlyso JavaScript cannot read them at all. - Web storage holds only strings, so objects go through
JSON.stringifyandJSON.parse.
localStorage.setItem("cart", JSON.stringify({ items: 2 }));
const cart = JSON.parse(localStorage.getItem("cart"));
console.log(cart.items); // 2
sessionStorage.setItem("step", "3");
localStorage.removeItem("cart");Interview tip: A common follow-up is where to store an auth token: anything in localStorage can be read by an injected script (XSS), which an HttpOnly cookie prevents.
JavaScript interview questions: intermediate level
What is a closure in JavaScript?
A closure is a function together with the variables from the scope where it was created; the function keeps access to those variables even after the outer function has returned.
Closures let you create private state, function factories, memoization caches and helpers such as debounce and once. The captured variables are kept by reference, not copied, so the inner function always sees their latest value. Because captured variables stay in memory as long as the function exists, a closure held by a long-lived listener or timer can keep large objects alive.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2
console.log(counter.count); // undefined (count is private)
How does the this keyword work 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 is written. Arrow functions are the exception: they take this from the surrounding scope.
- Method call
obj.fn():thisisobj. - Plain call
fn():undefinedin strict mode, the global object in non-strict mode. new Fn():thisis the newly created object.call,applyorbind:thisis the value you pass.- Event listener with a regular function:
thisis the element the listener is attached to.
const user = {
name: "Riya",
getName() { return this.name; },
};
console.log(user.getName()); // Riya
const fn = user.getName;
fn(); // `this` is lost: undefined in strict mode, so this.name throws a TypeError
const bound = user.getName.bind(user);
console.log(bound()); // Riya Interview tip: The "lost this" problem when a method is passed as a callback, for example setTimeout(user.getName, 100), is the follow-up most interviewers go for.
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 passed one by one, apply invokes it immediately with arguments passed as an array, and bind returns a new function with this (and optionally some arguments) fixed, to be called later.
function intro(city, role) {
return `${this.name} from ${city}, ${role}`;
}
const user = { name: "Riya" };
intro.call(user, "Pune", "Developer"); // "Riya from Pune, Developer"
intro.apply(user, ["Pune", "Developer"]); // "Riya from Pune, Developer"
const later = intro.bind(user, "Pune");
later("Tester"); // "Riya from Pune, Tester"bind is useful for passing methods as callbacks without losing this, and for partial application, as with later above. A bound function cannot be re-bound to a different this; calling bind on it again only adds arguments. Arrow functions ignore the this passed to any of the three.
What is a Promise, and what are its states?
A Promise is an object that represents the eventual result of an asynchronous operation. It is always in one of three states: pending, fulfilled or rejected. Once it settles (fulfilled or rejected), its state and value never change.
You attach handlers with then for success, catch for errors and finally for cleanup. Each then returns a new promise, so calls can be chained, and an error thrown anywhere in the chain skips the remaining then handlers and goes to the next catch. This flat chain replaces nested callbacks.
const wait = (ms) =>
new Promise((resolve) => setTimeout(() => resolve(ms), ms));
wait(500)
.then((ms) => {
console.log(`waited ${ms}ms`); // waited 500ms
throw new Error("Oops");
})
.then(() => console.log("skipped"))
.catch((err) => console.log(err.message)) // Oops
.finally(() => console.log("done")); // done Interview tip: A frequent follow-up: if a then callback returns another promise, the chain waits for that promise to settle before the next then runs.
How do async and await work, and how do you handle errors with them?
async/await is syntax built on top of promises that lets you write asynchronous code that reads like synchronous code. An async function always returns a promise, and await pauses that function until the awaited promise settles.
Errors are handled with ordinary try...catch. While the function is paused, the rest of the program keeps running, so await does not block the main thread. To run independent requests in parallel, start them together and await Promise.all instead of awaiting them one after another.
async function loadUser() {
try {
const res = await fetch("/api/user");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();
console.log(user.name);
} catch (err) {
console.error("Failed:", err.message);
} finally {
console.log("request finished");
}
} Interview tip: Point out the res.ok check: fetch rejects only on network failure, so a 404 or 500 response still resolves.
What is the event loop in JavaScript?
The event loop is the mechanism that lets single-threaded JavaScript handle asynchronous work: whenever the call stack is empty, it takes the next queued callback and runs it.
- Synchronous code runs on the call stack first, from top to bottom.
- Timers, network requests and DOM events are handled by the browser or Node.js; when they finish, their callbacks are queued.
- When the stack is empty, all pending microtasks (promise callbacks) run, then one macrotask (such as a
setTimeoutcallback), then microtasks again, and so on. Browsers can render between macrotasks.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// Output: A D C BEven with a 0 ms delay, the timeout callback has to wait until the stack is clear and all microtasks have run, which is why C prints before B.
What is the difference between microtasks and macrotasks, and what does this code print?
Microtasks are high-priority callbacks: promise reactions (then, catch, finally and code after await), queueMicrotask and MutationObserver. Macrotasks (also just called tasks) include setTimeout, setInterval, I/O and UI events. After each macrotask, the engine empties the entire microtask queue, including microtasks added while emptying it, before taking the next macrotask.
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => console.log(3));
}, 0);
Promise.resolve().then(() => {
console.log(4);
setTimeout(() => console.log(5), 0);
});
queueMicrotask(() => console.log(6));
console.log(7);
// Output: 1 7 4 6 2 3 5Sync code prints 1 and 7. The microtasks print 4 (which schedules a second timer) and 6. The first timer prints 2 and queues a microtask, which runs before the second timer, so 3 comes before 5.
Interview tip: In Node.js, process.nextTick callbacks run even before promise microtasks, which is a common follow-up for backend roles.
What is the difference between Promise.all, Promise.allSettled, Promise.race and Promise.any?
All four take an iterable of promises and return a single promise, but they settle differently.
Promise.all: fulfils with an array of all results in input order; rejects as soon as any one rejects.Promise.allSettled: waits for every promise and always fulfils with an array of{ status, value }or{ status, reason }objects.Promise.race: settles like the first promise to settle, whether it fulfils or rejects.Promise.any: fulfils with the first promise to fulfil; rejects with anAggregateErroronly if all of them reject.
const fast = new Promise((res) => setTimeout(() => res("fast"), 100));
const slow = new Promise((res) => setTimeout(() => res("slow"), 300));
const fail = new Promise((_, rej) => setTimeout(() => rej(new Error("fail")), 200));
Promise.all([fast, slow]).then(console.log); // ["fast", "slow"]
Promise.all([fast, fail]).catch((e) => console.log(e.message)); // fail
Promise.race([fast, fail]).then(console.log); // fast
Promise.any([fail, slow]).then(console.log); // slow
Promise.allSettled([fast, fail])
.then((r) => console.log(r.map((x) => x.status))); // ["fulfilled", "rejected"]Use all when every result is required, allSettled for independent jobs where partial failure is fine, race for timeouts, and any when one successful source is enough.
Interview tip: Mention that when Promise.all rejects early, the other operations are not cancelled; they keep running and their results are ignored.
What is event bubbling and event capturing?
When an event happens on an element, it travels in three phases: capturing (from window down to the target), the target phase, and bubbling (from the target back up to window). By default, listeners run in the bubbling phase.
Pass true or { capture: true } as the third argument of addEventListener to listen during capturing instead. Call event.stopPropagation() to stop the event travelling further. A few events, such as focus and blur, do not bubble; focusin and focusout are the bubbling versions.
// A button#child inside div#parent
const parent = document.querySelector("#parent");
const child = document.querySelector("#child");
parent.addEventListener("click", () => console.log("parent bubble"));
parent.addEventListener("click", () => console.log("parent capture"), true);
child.addEventListener("click", () => console.log("child"));
// Clicking the button logs:
// parent capture
// child
// parent bubble Interview tip: Know the difference between event.target (the element actually clicked) and event.currentTarget (the element whose listener is running).
What is event delegation, and why is it useful?
Event delegation means attaching one listener to a common parent instead of one listener on every child, then using event.target to work out which child triggered the event. It works because most events bubble up to the parent.
It uses fewer listeners and less memory, and it automatically works for children added later, such as new items in a to-do list. Use closest() so that clicks on elements nested inside an item, like an icon or a span, still match the item.
const list = document.querySelector("#todo-list");
list.addEventListener("click", (event) => {
const item = event.target.closest("li");
if (!item || !list.contains(item)) return;
item.classList.toggle("done");
});
// Items added later work without new listeners
list.insertAdjacentHTML("beforeend", "<li>Buy milk</li>");
What is the difference between a shallow copy and a deep copy?
A shallow copy duplicates only the top level of an object, so nested objects are still shared with the original; a deep copy duplicates every level, so the two are completely independent.
Spread syntax, Object.assign, Array.from and slice all make shallow copies. For a deep copy, use the built-in structuredClone, which handles nested objects, arrays, dates, maps, sets and even circular references, but throws on functions. The older JSON.parse(JSON.stringify(obj)) trick drops functions and undefined values and turns dates into strings.
const user = { name: "Riya", address: { city: "Pune" } };
const shallow = { ...user };
shallow.address.city = "Delhi";
console.log(user.address.city); // Delhi (nested object is shared)
const deep = structuredClone(user);
deep.address.city = "Mumbai";
console.log(user.address.city); // Delhi (original unchanged)
What is the difference between debounce and throttle, and how would you implement them?
Debounce delays running a function until a set time has passed since the last call, while throttle runs a function at most once in every set time interval.
Use debounce when only the final value matters, like a search box that calls the API after the user stops typing. Use throttle for continuous events where you want regular updates, like scroll or resize handlers. Both rely on a closure to keep the timer or timestamp between calls.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, interval) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) { last = now; fn.apply(this, args); }
};
} Interview tip: Writing debounce from memory is a very common machine-coding round task; be ready to explain why fn.apply(this, args) is used instead of fn().
What is the temporal dead zone (TDZ)?
The temporal dead zone is the period between the start of a scope and the line where a let, const or class is declared; accessing the name during this period throws a ReferenceError.
These declarations are hoisted, but unlike var they are not initialised until their declaration line runs. The TDZ catches bugs where a variable is used before it has a meaningful value. Even typeof is not safe inside the TDZ, although typeof on a name that was never declared returns "undefined".
console.log(a); // undefined
var a = 1;
// console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
let x = 10;
function test() {
console.log(x); // ReferenceError, not 10: the inner x shadows the outer one
let x = 20;
}
test();
What is type coercion, and what do these expressions print?
Type coercion is JavaScript automatically converting a value from one type to another when an operator needs it, for example a number to a string. Explicit conversion uses Number(), String() or Boolean().
+with a string on either side concatenates strings.- Other arithmetic operators such as
-,*and/convert their operands to numbers. - Objects and arrays are first converted to primitives, which for plain arrays and objects means strings.
console.log("5" + 3); // 53
console.log("5" - 3); // 2
console.log("5" * "2"); // 10
console.log(true + 1); // 2
console.log([] + []); // (empty string)
console.log([] + {}); // [object Object]
console.log([1, 2] + [3]); // 1,23
console.log(+""); // 0
console.log(1 + "2" - 1); // 11
How do ES modules work, and what is the difference between named and default exports?
ES modules let you split code into files that each have their own scope and share values explicitly with export and import. A file can have many named exports but at most one default export, which can be imported under any name.
// math.js
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// app.js
import multiply, { add, PI as pi } from "./math.js";
import * as math from "./math.js";
const { formatDate } = await import("./utils.js"); // dynamic import- Modules run in strict mode automatically and are evaluated only once, however many times they are imported.
- Imports are live, read-only bindings, not copies.
- In the browser they need
type="module"on the script tag and are deferred by default. - CommonJS (
requireandmodule.exports) is the older Node.js system; staticimportstatements can be analysed at build time, which enables tree shaking.
JavaScript interview questions: advanced level
What is the prototype chain, and how does prototypal inheritance work?
Every JavaScript object has an internal link to another object called its prototype. When you read a property the object does not have, JavaScript looks for it on the prototype, then on the prototype's prototype, and so on until it reaches null. This chain is the prototype chain, and it is how inheritance works in JavaScript.
Regular functions have a prototype property, and objects created with new get that object as their prototype, so methods placed there are shared by all instances instead of being copied. Use Object.getPrototypeOf(obj) to read it and Object.create(proto) to set it; __proto__ is a legacy accessor.
function Person(name) { this.name = name; }
Person.prototype.greet = function () { return `Hi, I am ${this.name}`; };
const riya = new Person("Riya");
console.log(riya.greet()); // Hi, I am Riya
console.log(Object.getPrototypeOf(riya) === Person.prototype); // true
console.log(Object.hasOwn(riya, "greet")); // false
const admin = Object.create(riya);
console.log(admin.name); // Riya (found on the prototype)
Are JavaScript classes just syntactic sugar over constructor functions?
Mostly, but not completely. Classes use the same prototype model underneath: methods still live on the prototype and extends sets up the prototype chain. There are, however, real behavioural differences.
- A class must be called with
new; calling it like a normal function throws a TypeError. - Class bodies always run in strict mode.
- Class declarations stay in the temporal dead zone, so they cannot be used before the line where they are declared.
- Class methods are non-enumerable, so they do not show up in
for...inloops. - Classes support truly private
#fields, static members andsupercalls.
class Account {
#balance = 0;
static bank = "Demo Bank";
deposit(amount) { this.#balance += amount; return this.#balance; }
}
class Savings extends Account {
constructor(rate) { super(); this.rate = rate; }
}
const s = new Savings(4);
console.log(s.deposit(500)); // 500
console.log(Savings.bank); // Demo Bank (static members are inherited)
// s.#balance outside the class is a SyntaxError
How would you implement Promise.all yourself?
Return a new promise, store each result at the same index as its input, count how many inputs have fulfilled, resolve when the count reaches the input length, and reject on the first rejection.
function promiseAll(items) {
return new Promise((resolve, reject) => {
const list = Array.from(items);
const results = new Array(list.length);
let done = 0;
if (list.length === 0) return resolve([]);
list.forEach((item, i) => {
Promise.resolve(item).then((value) => {
results[i] = value;
if (++done === list.length) resolve(results);
}, reject);
});
});
}The details interviewers check: results keep input order even when later promises finish first, plain values are wrapped with Promise.resolve, an empty input resolves immediately with [], and calling reject more than once is harmless because a promise can settle only once.
How would you write a polyfill for Function.prototype.bind?
Add a method to Function.prototype that remembers the original function, the this value and any preset arguments, and returns a new function that calls the original with apply, combining the preset arguments with the ones passed later.
Function.prototype.myBind = function (context, ...presetArgs) {
const fn = this;
return function (...laterArgs) {
return fn.apply(context, [...presetArgs, ...laterArgs]);
};
};
const user = { name: "Riya" };
function greet(greeting, mark) {
return `${greeting}, ${this.name}${mark}`;
}
const hello = greet.myBind(user, "Hello");
console.log(hello("!")); // Hello, Riya!This covers what most interviews expect. The native bind also handles a bound function being called with new: in that case the bound this is ignored and a new object is created. A fuller polyfill checks new.target for that case. Interviewers may also ask for call or apply polyfills, which temporarily attach the function to the context object and invoke it as a method.
How does error handling work in JavaScript, including asynchronous code?
Synchronous errors are handled with try...catch...finally, where finally runs whether or not an error occurred. You can throw any value, but throwing Error objects or subclasses gives you a message and a stack trace.
try...catch only catches errors thrown while the try block is running, so an error thrown later inside a setTimeout callback escapes it. For promises, use .catch() or await inside try...catch. An unhandled rejection fires the unhandledrejection event in browsers, and in current Node.js versions it crashes the process by default.
class ValidationError extends Error {
constructor(message) { super(message); this.name = "ValidationError"; }
}
try {
throw new ValidationError("Age must be positive");
} catch (err) {
if (err instanceof ValidationError) console.log(err.name, err.message);
else throw err;
}
// ValidationError Age must be positive
try {
setTimeout(() => { throw new Error("late"); }, 0);
} catch (e) {
console.log("never runs"); // the error is thrown after try has finished
} Interview tip: Mention new Error(message, { cause: err }) for wrapping a low-level error without losing the original one.
What will this code print when this is used inside callbacks and extracted methods?
It prints undefined, Alpha, true, Beta and Alpha, because each call sets this in a different way.
class Team {
constructor() { this.name = "Alpha"; }
regular() { return [1].map(function () { return this; })[0]; }
arrow() { return [1].map(() => this.name)[0]; }
method() { return this.name; }
}
const t = new Team();
console.log(t.regular()); // undefined
console.log(t.arrow()); // Alpha
const m = t.method;
try { m(); } catch (e) { console.log(e instanceof TypeError); } // true
console.log(t.method.call({ name: "Beta" })); // Beta
console.log(m.bind(t)()); // AlphaClass bodies always run in strict mode, so a plain function callback gets this as undefined rather than the instance. The arrow callback has no this of its own and takes it from arrow(), which was called on t. Assigning t.method to m detaches it from the object, so calling m() makes this undefined and reading this.name throws a TypeError. call and bind set this explicitly, so they print Beta and Alpha.
What does a setTimeout inside a var loop print, and how do you fix it?
The var loop prints 3 three times; switching to let, or capturing the value in a new scope, prints 0, 1 and 2.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3 3 3
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// 0 1 2
for (var k = 0; k < 3; k++) {
((n) => setTimeout(() => console.log(n), 100))(k);
}
// 0 1 2Treat each loop as if it ran on its own. With var there is only one i for the whole function. The callbacks run after the loop has finished, when i is already 3, so all three print 3. let creates a fresh binding for every iteration, so each callback closes over its own value. Before let existed, the fix was an IIFE that captured the current value in a new function scope, as in the third loop, or passing the value as an extra argument to setTimeout.
What is currying, and how would you implement sum(1)(2)(3)?
Currying transforms a function that takes several arguments into a sequence of functions that each take one (or some) of them. It relies on closures to remember the arguments already supplied, and it is useful for building specialised functions from general ones.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn.apply(this, args);
return (...next) => curried.apply(this, [...args, ...next]);
};
}
const add3 = (a, b, c) => a + b + c;
const sum = curry(add3);
console.log(sum(1)(2)(3)); // 6
console.log(sum(1, 2)(3)); // 6
// Unlimited version, ended by an empty call
const sumAll = (a) => (b) => (b === undefined ? a : sumAll(a + b));
console.log(sumAll(1)(2)(3)()); // 6fn.length gives the number of declared parameters, which tells the curried function when enough arguments have arrived. It does not count rest parameters and stops counting at the first parameter that has a default value.
What is the difference between const, Object.freeze and Object.seal?
const only stops a variable from being reassigned; the object it points to can still change. Object.freeze makes the object itself unchangeable: no adding, removing or modifying properties. Object.seal blocks adding and removing properties but still allows changing existing ones.
Both freeze and seal are shallow, so nested objects stay mutable unless you freeze them too, usually with a recursive deep-freeze helper. In non-strict code, a blocked change fails silently; in strict code, including modules and classes, it throws a TypeError.
// Non-strict script
const config = Object.freeze({ env: "prod", db: { port: 5432 } });
config.env = "dev"; // silently ignored
config.db.port = 3306; // works: freeze is shallow
console.log(config.env, config.db.port); // prod 3306
const user = Object.seal({ name: "Riya" });
user.name = "Aman"; // allowed
user.age = 22; // silently ignored
delete user.name; // silently ignored (returns false)
console.log(user.name, user.age); // Aman undefined
What is the output of this async/await and Promise code?
It prints 4, 1, 3, 6, 8, 2, 7 and finally 5.
async function first() {
console.log("1");
await second();
console.log("2");
}
async function second() { console.log("3"); }
console.log("4");
setTimeout(() => console.log("5"), 0);
first();
new Promise((resolve) => { console.log("6"); resolve(); })
.then(() => console.log("7"));
console.log("8");
// Output: 4 1 3 6 8 2 7 5Synchronous code runs first: 4, then first() runs synchronously up to its first await, printing 1, and calling second() runs its body immediately, printing 3. A Promise executor also runs synchronously, so 6 prints, then 8. Next the microtasks run in the order they were queued: the rest of first after the await (2), then the then callback (7). The timeout is a macrotask, so 5 comes last.
Interview tip: The key insight is that an async function runs synchronously until its first await; only the code after the await is deferred.
How good is your JavaScript, really?
Take the 15-question HTML, CSS and JavaScript challenge. Miss one and you get a short explanation plus a second chance to win the point back. At the end you see exactly where you stand, and which class to take next.
- 15 questions
- About 10 minutes
- 100% free