JavaScript Interview Questions and Answers
26 questions8 beginner · 12 intermediate · 6 advancedLast reviewed
This set is for front-end, full-stack and Node.js candidates, from first-job screens to senior rounds. It focuses on the questions that keep coming back: scoping, closures, this, the event loop, promises, and a few TypeScript questions that frequently follow.
Practise by answering aloud in about a minute, then read the model answer and the follow-up. Paste each snippet into Node or the browser console and predict the output before running it. For a structured walkthrough, the JavaScript fundamentals course and the TypeScript course cover the same ground.
1What are the differences between
var,letandconst?Beginnervaris function-scoped, can be redeclared, and is hoisted and initialised toundefined.letandconstare block-scoped, cannot be redeclared in the same scope, and sit in the temporal dead zone until their declaration runs.constforbids reassigning the binding, but it does not freeze the value.In modern code, default to
const, useletwhen you must reassign, and avoidvar. Theconstpoint trips people up: aconstobject or array can still be mutated, because only the variable's link to it is fixed. UseObject.freezeif you want shallow immutability.What the interviewer is checking: Scope rules plus the common misunderstanding that const means immutable.
Likely follow-up: Where does a variable declared with
varinside anifblock end up visible?2What is hoisting, and what is the temporal dead zone?Beginner
Hoisting means declarations are processed before code runs, so a name exists from the start of its scope. Function declarations are hoisted with their body, so you can call them before they appear.
varis hoisted and initialised toundefined, so reading it early givesundefined.letandconstare also hoisted, but not initialised until execution reaches the declaration. Accessing them in between throws aReferenceError; that gap is the temporal dead zone. It turns a silentundefinedbug into a loud error, which is one reason to prefer them. Function expressions assigned to a variable follow the variable's rules, not the function-declaration rules.javascript console.log(early); // undefined, not an error var early = 1; function hoisted() { return "ok"; } console.log(hoisted()); try { console.log(tdz); } catch (e) { console.log(e.name + ": " + e.message); } let tdz = 1;Output
undefined ok ReferenceError: Cannot access 'tdz' before initialization
What the interviewer is checking: Whether you describe hoisting accurately instead of saying code moves to the top.
Likely follow-up: Is a function expression like
const f = function(){}hoisted the same way as a declaration?3Explain the difference between
==and===. Which do you use and why?Beginner===compares without conversion: different types are never equal.==first coerces operands to a common type using a set of rules, which produces surprising results such as0 == ""being true and[] == falsebeing true.Default to
===everywhere so behaviour is predictable. The one idiom some teams accept isx == null, which is true for bothnullandundefined. Remember also thatNaNis not equal to anything, including itself, so useNumber.isNaNorObject.isfor that check.Object.isalso distinguishes0from-0.What the interviewer is checking: Awareness of coercion pitfalls and a sensible team convention.
Likely follow-up: Why is
null >= 0true whilenull == 0is false?4What is the difference between
nullandundefined, and how do??and?.help?Beginnerundefinedmeans a value was never assigned, such as a missing property or an omitted argument.nullis an explicit "no value" that a programmer assigns.typeof nullis"object", a long-standing quirk of the language, so check for it with=== null.?.short-circuits toundefinedwhen the left side isnullorundefined, avoiding long guard chains.??falls back only fornullorundefined, unlike||, which also replaces0,""andfalse. That makes??the correct way to default a count of zero or an empty string that is a valid value.What the interviewer is checking: Whether you pick
??over||for the right reason.Likely follow-up: What does
a ??= 5do compared toa ||= 5?5What is a closure? Give a practical use.Beginner
A closure is a function bundled with the variables from the scope where it was created, which stay accessible even after that outer function has returned. The inner function keeps a live reference to those variables, not a copy of their values.
A practical use is private state: a factory returns functions that share a variable no outside code can touch. Closures also power callbacks, memoisation, partial application and module patterns. The risk is accidentally retaining large objects for as long as the function lives, which is a classic source of memory leaks.
javascript function makeCounter() { let n = 0; return { inc: () => ++n, get: () => n }; } const c = makeCounter(); c.inc(); c.inc(); console.log(c.get()); // 2 console.log(c.n); // undefined: n is not reachable from outsideOutput
2 undefined
What the interviewer is checking: A precise definition plus a concrete, non-trivial use.
Likely follow-up: How could a closure cause a memory leak?
6How does
thisget its value in JavaScript?BeginnerFor normal functions,
thisis decided by how the function is called, not where it is defined.obj.method()setsthistoobj. A plain callfn()givesundefinedin strict mode (or the global object in sloppy mode).new Fn()creates a new object asthis.call,applyandbindset it explicitly.The classic bug is detaching a method and passing it as a callback, which loses
this. Fix it withbind, an arrow function wrapper, or by defining the handler as an arrow-function property. Arrow functions do not have their ownthis; they use the one from the surrounding scope.javascript "use strict"; const desk = { label: "D1", regular() { return this?.label; }, delayed() { return [1].map(() => this.label); }, }; console.log(desk.regular(), desk.delayed()); const detached = desk.regular; console.log(detached()); console.log(desk.regular.call({ label: "D2" })); const bound = desk.regular.bind({ label: "D3" }); console.log(bound.call({ label: "x" }));Output
D1 [ 'D1' ] undefined D2 D3
What the interviewer is checking: The call-site rule, and awareness of lost-this bugs and arrow-function behaviour.
Likely follow-up: Can you rebind
thison an arrow function usingcall?7Why does
[10, 1, 5].sort()return[1, 10, 5], and how do you sort numbers properly?BeginnerWithout a comparator,
sortconverts elements to strings and compares them in UTF-16 order, so "10" sorts before "5". For numbers, pass a comparator:(a, b) => a - bfor ascending order. The comparator returns a negative, zero or positive number.Also note that
sortmutates the original array. To avoid that, copy first with[...nums].sort(...), or usetoSorted, available in recent Node and browser versions. Typical array companions aremapto transform,filterto select andreduceto combine; always givereducean initial value, or it throws on an empty array.What the interviewer is checking: Knowing default sort semantics and mutation, rather than just using sort blindly.
Likely follow-up: Which array methods mutate the original array and which return a new one?
8Why does
0.1 + 0.2 === 0.3return false in JavaScript?BeginnerJavaScript numbers are IEEE 754 double-precision floats. Many decimal fractions, such as 0.1 and 0.2, have no exact binary representation, so the sum carries a tiny rounding error and equals 0.30000000000000004 rather than exactly 0.3. This is not unique to JavaScript.
For comparisons, check that the difference is within a small tolerance such as
Number.EPSILONfor values near 1. For money, avoid floats: store integer minor units like cents, or use a decimal library. Integers are exact up toNumber.MAX_SAFE_INTEGER; beyond that, useBigInt.What the interviewer is checking: Whether you know it is a floating-point property and how to handle money safely.
Likely follow-up: What does
toFixedreturn, and why can it surprise you?9What is the difference between arrow functions and regular functions?Intermediate
Arrow functions have no own
this,arguments,superornew.target; they take them from the enclosing scope. They also cannot be used as constructors withnew, and they have noprototype. Regular functions have all of these.That makes arrows ideal for callbacks and array methods inside a method, where you want to keep the outer
this. Avoid them as object methods that needthisto be the object, and as prototype methods or event handlers that rely on the element beingthis. Use rest parameters(...args)in place ofargumentsin arrows.What the interviewer is checking: That you tie the difference to concrete consequences, not just shorter syntax.
Likely follow-up: What does an arrow function return if its body is wrapped in braces with no
return?10How does prototypal inheritance work, and how do ES6 classes relate to it?Intermediate
Every object has an internal link to a prototype object. When you read a property, the engine looks on the object, then on its prototype, then the prototype's prototype, until it finds it or reaches
null. Methods shared by all instances live on the prototype, not on each instance.ES6
classis syntax over this mechanism. Methods go ontoClass.prototype,extendslinks the prototype chains, andsupercalls up the chain. Differences worth knowing: class constructors throw if called withoutnew, and class bodies run in strict mode.Object.create(proto)is the direct way to build an object with a chosen prototype.What the interviewer is checking: Whether you understand the chain lookup rather than treating classes as a separate system.
Likely follow-up: What is the difference between
Object.keysand afor...inloop with respect to the prototype chain?11Why does a loop with
varandsetTimeoutprint the same number each time, and how doesletfix it?Intermediatevaris function-scoped, so the whole loop shares a singlei. Every callback closes over that one variable, and by the time they run the loop has ended andiholds its final value.letin aforheader creates a fresh binding for each iteration, so each callback captures its own value. Beforeletexisted, people wrapped the body in an immediately invoked function to create a new scope. The deeper lesson is that closures capture variables, not values, so always ask which variable a callback is actually referring to.javascript const withVar = []; for (var i = 0; i < 3; i++) withVar.push(() => i); console.log(withVar.map(f => f())); const withLet = []; for (let j = 0; j < 3; j++) withLet.push(() => j); console.log(withLet.map(f => f()));Output
[ 3, 3, 3 ] [ 0, 1, 2 ]
What the interviewer is checking: Applying the closure model to a familiar puzzle and explaining why, not just the fix.
Likely follow-up: Name another way to fix this without using
let.12Explain the event loop. In what order do synchronous code, promise callbacks and
setTimeoutcallbacks run?IntermediateJavaScript runs on one thread with a call stack. Async work such as timers and I/O is handled by the host environment, and finished callbacks are queued. When the stack is empty, the loop runs all pending microtasks (promise reactions,
queueMicrotask, code afterawait), then takes the next macrotask (a timer, an I/O callback, a UI event).So the order is: current synchronous code, then every queued microtask, then the next macrotask. A
setTimeout(fn, 0)therefore runs after promise callbacks, and the delay is a minimum, not a guarantee. A long synchronous loop blocks everything, including rendering and timers.javascript console.log("1 sync"); setTimeout(() => console.log("5 timeout"), 0); Promise.resolve().then(() => console.log("3 promise")); queueMicrotask(() => console.log("4 microtask")); (async () => { console.log("2 sync part of async fn"); await null; console.log("4b after await"); })(); console.log("2b sync end");Output
1 sync 2 sync part of async fn 2b sync end 3 promise 4 microtask 4b after await 5 timeout
What the interviewer is checking: A correct model of microtasks versus macrotasks and the fact that async functions start synchronously.
Likely follow-up: Can microtasks starve the event loop? Give an example.
13What is the difference between
Promise.all,allSettled,raceandany?IntermediatePromise.allresolves with all results once every promise fulfils, but rejects immediately on the first rejection.allSettledwaits for everything and reports each outcome as fulfilled or rejected, so it never rejects.racesettles with the first promise to settle, success or failure.anyfulfils with the first fulfilment and rejects only if all reject.Choose by failure policy:
allfor all-or-nothing,allSettledfor independent best-effort work like loading dashboard widgets,racefor timeouts,anyfor the fastest successful mirror. Note that whenallrejects, the other operations are not cancelled, they just have their results ignored.What the interviewer is checking: Picking the combinator from the failure semantics you need.
Likely follow-up: How would you add a timeout to a fetch using one of these?
14What is wrong with
return load()inside atry/catchin an async function, and withforEach(async ...)?Intermediatereturn load()returns the promise without awaiting it, so if it rejects, the rejection happens after the function has left thetryblock and yourcatchnever sees it. Usereturn await load()when you want to catch errors locally.forEachignores the promises returned by an async callback, so it neither waits for them nor surfaces their errors; code after the loop runs first. Usefor...ofwithawaitfor sequential work, orawait Promise.all(items.map(...))to run in parallel. Any promise that is created and not awaited or caught risks an unhandled rejection.javascript async function load() { throw new Error("db down"); } async function a() { try { return load(); } catch (e) { return "caught-a"; } } async function b() { try { return await load(); } catch (e) { return "caught-b"; } } a().then(console.log).catch(e => console.log("a rejected:", e.message)); b().then(console.log); [1, 2, 3].forEach(async n => { await null; console.log("forEach", n); }); console.log("after forEach");Output
after forEach forEach 1 forEach 2 forEach 3 caught-b a rejected: db down
What the interviewer is checking: Real-world async/await error-handling judgement, not just syntax.
Likely follow-up: When is sequential awaiting in a loop the right choice over
Promise.all?15How do you copy an object in JavaScript? When do spread,
JSONandstructuredClonediffer?IntermediateSpread (
{ ...obj }) andObject.assignmake a shallow copy: nested objects are shared, so mutating a nested field affects both. For a deep copy,structuredClonehandles nested objects, arrays,Date,Map,Setand cyclic references, but throws on functions.The
JSON.parse(JSON.stringify(x))trick is lossy: it drops functions andundefined, turnsDateinto a string, and fails on cycles andBigInt. For nested state updates, such as in a UI store, a common approach is to copy only the path you change rather than deep-cloning everything.javascript const o = { id: 1, when: new Date(0), nested: { k: 1 }, fn() {} }; const sh = { ...o }; sh.nested.k = 2; console.log(o.nested.k); const sc = structuredClone({ when: new Date(0), nested: { k: 1 } }); console.log(sc.when instanceof Date); console.log(typeof JSON.parse(JSON.stringify(o)).when); try { structuredClone(o); } catch (e) { console.log(e.name); }Output
2 true string DataCloneError
What the interviewer is checking: Knowing the trade-offs of each copy approach, especially the hidden losses in JSON cloning.
Likely follow-up: How would you update one nested field immutably without cloning the whole object?
16What is the difference between debouncing and throttling? Implement a debounce.Intermediate
Debounce delays a call until the events have stopped for a given time, so a burst produces a single call at the end. It suits search-as-you-type or autosave. Throttle guarantees a call at most once per interval while events continue, so it suits scroll or resize handlers where you still want regular updates.
A debounce keeps a timer in a closure: each call clears the old timer and sets a new one. Mention edge cases: leading versus trailing execution, a
cancelmethod for cleanup, and preservingthisand arguments. In React, create the debounced function once, not on every render.javascript function debounce(fn, ms) { let id; return (...a) => { clearTimeout(id); id = setTimeout(() => fn(...a), ms); }; } function throttle(fn, ms) { let last = -Infinity; return (...a) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...a); } }; } const d = debounce(x => console.log("debounced", x), 50); const t = throttle(x => console.log("throttled", x), 50); for (let i = 1; i <= 5; i++) { d(i); t(i); } setTimeout(() => t(6), 80);Output
throttled 1 debounced 5 throttled 6
What the interviewer is checking: Clear distinction of the two patterns plus a working closure-based implementation.
Likely follow-up: How would you add a leading-edge option to your debounce?
17What is the difference between CommonJS and ES modules?Intermediate
CommonJS uses
require()andmodule.exports, loads synchronously and evaluates at runtime, so you can callrequireconditionally. ES modules use staticimportandexport, which are analysed before execution. That enables tree-shaking, top-levelawaitand live bindings, where an imported name reflects later changes in the exporting module.In an ES module file,
require,moduleand__dirnamedo not exist, andimport.meta.urlreplaces them. In Node, a file is treated as ESM based on the.mjsextension or"type": "module"in package.json. You can load an ES module from CommonJS using dynamicimport(), which returns a promise.What the interviewer is checking: Understanding static versus dynamic loading and the practical interop rules.
Likely follow-up: Why does tree-shaking work with ES modules but not reliably with CommonJS?
18What are
#privateclass fields, and how are they different from the underscore convention?IntermediateA field named with
#, like#raw, is truly private to the class body: outside code cannot read or write it, and the engine enforces this. A leading underscore is only a naming convention, and anyone can still accessobj._raw. Private fields also do not appear inObject.keysorJSON.stringify.Accessing
obj.#rawoutside the class is a syntax error, not justundefined. Combine private fields with getters and setters to validate changes. Before this feature, closures orWeakMapwere used for the same purpose. Static fields, written withstatic, belong to the class itself rather than each instance.What the interviewer is checking: Familiarity with modern class features and real encapsulation.
Likely follow-up: What does a getter without a setter do when you assign to the property?
19In TypeScript, what is the difference between
typeandinterface? Which do you choose?IntermediateBoth describe object shapes and are largely interchangeable for that. Interfaces can be extended with
extendsand can be reopened and merged through declaration merging, which is how libraries let you augment their types. Atypealias can also name unions, intersections, tuples, primitives and mapped or conditional types, which an interface cannot.A practical rule: use
interfacefor public object shapes and class contracts, especially in libraries, andtypewhen you need unions or computed types. Whatever you pick, be consistent in the codebase. Neither exists at runtime, since types are erased when the code compiles to JavaScript.What the interviewer is checking: Knowing the real differences instead of repeating that they are the same.
Likely follow-up: What does it mean that types are erased at runtime, and what follows from that?
20What is the difference between
anyandunknownin TypeScript?Intermediateanyturns off type checking for a value: you can call it, index it or assign it anywhere without errors, and the unsafety spreads to everything it touches.unknownmeans 'a value of a type I do not know yet'; you can assign anything to it, but must narrow it with a check such astypeof,instanceofor a validator before using it.Use
unknownfor data from outside your control, such as parsed JSON,catchvariables or API responses, and validate it, ideally with a schema library, before trusting its shape. Reserveanyfor gradual migration, and prefer a narrow cast with a comment over a blanket one. EnablingstrictandnoImplicitAnystops accidentalany.What the interviewer is checking: Whether you treat type safety at boundaries as a habit, not a formality.
Likely follow-up: How do you type the
catch (e)variable safely?21What are generics in TypeScript? Show a case where a constraint like
K extends keyof Thelps.AdvancedGenerics let a function or type work over many types while keeping the relationship between inputs and outputs.
first<T>(xs: T[]): T | undefinedreturns a number for a number array and a string for a string array, whichanycould not express.Constraints narrow what a type parameter may be. In
pick<T, K extends keyof T>(o: T, k: K): T[K],Kmust be a real key ofT, so a typo is a compile error and the return type is exactly that property's type. Generics are also used for reusable containers, API response wrappers and typed hooks. Avoid generic parameters that appear only once; they usually add noise, not safety.typescript function first<T>(xs: readonly T[]): T | undefined { return xs[0]; } function pick<T, K extends keyof T>(o: T, k: K): T[K] { return o[k]; } console.log(first([1, 2]), first(["a"]), first([])); console.log(pick({ id: 1, name: "x" }, "name")); // pick(..., "nope") is a compile errorOutput
1 a undefined x
What the interviewer is checking: Understanding generics as relationships between types, plus constraints and keyof.
Likely follow-up: What is the difference between a generic constraint and a default type parameter?
22How do discriminated unions and exhaustive checks make TypeScript code safer?Advanced
A discriminated union is a union of object types that share a literal-typed tag, such as
kind: "circle" | "rect". When you check the tag in aniforswitch, the compiler narrows the variable to the matching member, so you can access that member's fields safely.For exhaustiveness, assign the leftover value to a variable of type
neverin thedefaultbranch. If someone adds a new member to the union later, everyswitchthat forgot to handle it stops compiling. This models states like loading, success and error in UI code far better than a bag of optional fields, and removes impossible combinations.typescript type Shape = | { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.round(Math.PI * s.r ** 2); case "rect": return s.w * s.h; default: { const _never: never = s; // compile error if a case is missing return _never; } } } console.log(area({ kind: "rect", w: 2, h: 3 }), area({ kind: "circle", r: 2 }));Output
6 13
What the interviewer is checking: Using the type system to model states and make illegal states unrepresentable.
Likely follow-up: Why is this better than a single interface with optional fields?
23In Node.js, what is the order of
process.nextTick, promise callbacks andsetTimeout?AdvancedAfter the current synchronous code finishes, Node drains the
process.nextTickqueue first, then the promise microtask queue, and only then moves on to the event loop phases where timers and I/O callbacks run. So nextTick callbacks come before promise callbacks, which come before asetTimeout(..., 0).Both queues are drained fully before the loop continues, so a callback that keeps scheduling more nextTicks or microtasks can starve timers and I/O. Prefer promises or
queueMicrotaskfor most deferral, andsetImmediatewhen you want to yield to the I/O phase. The relative order ofsetTimeout(0)andsetImmediatefrom the main module is not guaranteed.What the interviewer is checking: Node-specific event-loop depth and awareness of starvation risks.
Likely follow-up: How would you process a huge array without blocking the loop?
24You need to call an API for 1,000 items but with at most 5 requests in flight. How do you write that?Advanced
Firing all 1,000 with
Promise.alloverwhelms the server or hits rate limits, and awaiting one by one is needlessly slow. Use a small worker pool: start N lanes that each pull the next index from a shared counter, await the job, and loop until the list is empty. Thenawait Promise.allon the lanes.Writing results by index preserves input order. Decide the failure policy up front: a rejection in one lane makes
Promise.allreject, while retries or collecting per-item errors need a try/catch around each job. Add a retry with backoff for transient failures. In production you might use a tested library such as p-limit instead of rolling your own.javascript async function mapLimit(items, limit, worker) { const results = new Array(items.length); let next = 0; async function lane() { while (next < items.length) { const i = next++; results[i] = await worker(items[i]); } } const lanes = Array.from({ length: Math.min(limit, items.length) }, lane); await Promise.all(lanes); return results; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); mapLimit([1, 2, 3, 4, 5, 6], 2, async (n) => { await sleep(10 * n); return n * n; }).then(console.log);Output
[ 1, 4, 9, 16, 25, 36 ]
What the interviewer is checking: Designing bounded concurrency from first principles and thinking about ordering and failures.
Likely follow-up: How would you make one failed item not abort the others?
25A search box fires a request per keystroke and old responses sometimes overwrite newer ones. How do you fix it?Advanced
This is a race condition: responses can arrive out of order, so the last response to arrive wins instead of the response to the latest query. The fix is to ensure only the most recent request is allowed to update the UI.
Cancel stale requests with an
AbortController(pass its signal tofetch, abort the previous one on each new query), or tag each request with an incrementing id and ignore results whose id is no longer the latest. Combine this with a debounce to send fewer requests. In React, do the same cleanup in the effect's return function so an unmounted component does not set state.javascript const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let latest = 0; async function search(term, delay) { const id = ++latest; await sleep(delay); // stand-in for fetch() if (id !== latest) return console.log("ignored stale result for", term); console.log("render results for", term); } search("pi", 50); // slow response search("pizza", 10); // newer, faster responseOutput
render results for pizza ignored stale result for pi
What the interviewer is checking: Recognising async race conditions and knowing cancellation and ordering strategies.
Likely follow-up: What is the difference between aborting a request and ignoring its result?
26Where do memory leaks come from in JavaScript apps, and how would you find one?Advanced
The garbage collector frees objects that are unreachable, so a leak means something still references data you no longer need. Common causes are event listeners or subscriptions never removed, timers (
setInterval) never cleared, caches or maps that grow without bound, closures holding large objects, and detached DOM nodes still referenced from JavaScript.To find one, compare heap snapshots in the browser's DevTools (or Node's inspector) taken before and after repeating an action, and look at what is growing and what retains it. Fixes: remove listeners and clear timers on teardown, bound your caches, and use
WeakMaporWeakRefwhen a mapping should not keep the key alive.What the interviewer is checking: Practical diagnosis skills: the usual retention paths and the tooling to spot them.
Likely follow-up: When would you reach for
WeakMapinstead ofMap?
Fill the gaps with a free course
If one of these JavaScript answers felt shaky, these 2 courses cover the same ground step by step: JavaScript Fundamentals, TypeScript.
More interview questions
- Java
- Spring Boot
- Python
- React
- Node.js
- C++
- Data Structures & Algorithms
- SQL & Databases
- System Design
- DevOps
- AWS & Cloud
- Security
- Generative AI & LLMs
- Machine Learning
- Behavioral & HR
Practising JavaScript for a real opening? Upload your resume to see which open jobs list JavaScript skills you already have, or browse open roles.