23 Senior JavaScript Interview Questions and Answers (2026)

JavaScript runs the web. Almost every site, app, and tool you touch depends on it, and if you build for the browser or Node you are expected to know it well. Plenty of engineers ship JavaScript every day without understanding hoisting, the event loop, or how the V8 engine handles Promises.
Real depth in JavaScript is rare, and it is what helps you stand out from every other senior candidate. If you study these and know them well you will be in a great position to land the offer.
Q1.What is the difference between Lexical Scope and Dynamic Scope?
Lexical (static) scope resolves variables based on where code is written in the source; dynamic scope would resolve them based on where a function is called from at runtime. JavaScript uses lexical scope.
Lexical scope:
A function's accessible variables are determined by its physical nesting at definition time, not by who calls it.
This is what enables closures: an inner function remembers the scope it was defined in.
Dynamic scope:
Variables would be resolved by walking the call stack, so the same function could see different values depending on the caller.
Used by some languages (e.g. older Lisp, Bash), but not JavaScript.
this is the exception that feels "dynamic": its value depends on how a regular function is called, unlike variable lookup.
Q2.Why does hoisting happen in JavaScript? Explain how the engine handles function declarations vs. variable declarations during the creation phase.
Hoisting happens because the engine processes code in two passes: a creation phase that registers declarations in scope before any code runs, and an execution phase that runs statements top to bottom. Function declarations are fully hoisted (name and body), while variable declarations only hoist the binding.
Why it happens: During the creation phase the engine scans the scope and allocates every declared identifier before execution, so references resolve to known bindings.
Function declarations: Fully hoisted: the entire function is available before its line, so you can call it earlier in the file.
var declarations: The name is hoisted and initialized to undefined; the assignment stays in place at the original line.
let/const declarations: The binding is hoisted but left uninitialized (TDZ), so access before the line throws.
Function expressions and arrow functions follow their variable's rules, not the function-declaration rule.
Q3.Why does [] + [] result in an empty string?
[] + [] result in an empty string?Because + requires primitives, JavaScript coerces each array to a primitive string. An empty array becomes the empty string, so "" + "" is "".
Step 1: ToPrimitive: The + operator converts each operand to a primitive, calling valueOf() (returns the array itself, not a primitive) then toString().
Step 2: array toString: An array's toString() joins elements with commas; an empty array yields "".
Step 3: string concatenation: With both now strings, + concatenates: "" + "" is "".
Contrast: [] + {} is "[object Object]" because the object stringifies that way.
Q4.When would you use Object.create(null) instead of a standard object literal?
Object.create(null) instead of a standard object literal?Use Object.create(null) when you want a truly empty object with no inherited prototype, typically for a clean key/value map or dictionary.
No inherited properties:
A literal {} inherits from Object.prototype, so keys like toString or hasOwnProperty already exist and can collide with user data.
With a null prototype there is no inherited noise, so any key you check is genuinely your own.
Safer as a hash map:
You can write key in map without worrying about inherited names returning true.
Avoids prototype-pollution-style bugs from a key named __proto__.
Trade-offs:
It has no methods, so obj.hasOwnProperty(k) fails: use Object.prototype.hasOwnProperty.call(obj, k).
For most maps a real Map is the cleaner modern choice; reach for Object.create(null) when you specifically need a plain object shape.
Q5.Why does a Promise resolve before a setTimeout(0)?
Promise resolve before a setTimeout(0)?Because Promise callbacks run as microtasks while setTimeout schedules a macrotask, and the event loop drains the entire microtask queue before picking up the next macrotask: even a 0ms timer waits.
Two different queues:
Microtasks: resolved Promise reactions (.then callbacks), queueMicrotask.
Macrotasks: setTimeout, setInterval, I/O callbacks.
The loop's priority rule:
After the current synchronous code finishes, the engine empties all pending microtasks before running even one macrotask.
So the Promise callback always runs first, regardless of the timer's delay.
The 0ms is also a floor, not a guarantee: Timers have a minimum clamp and only fire when their macrotask phase is reached, reinforcing the ordering.
Q6.What happens if a Promise is never resolved or rejected, and how does the engine handle this hanging state?
Promise is never resolved or rejected, and how does the engine handle this hanging state?A Promise that is never settled stays pending forever: any await or .then() on it simply never fires. The engine does not error or time out; it just holds the state until garbage collected.
It hangs silently:
Callbacks attached to it are never queued, so an async function awaiting it never resumes.
No exception is thrown; there is no built-in timeout in the spec.
Memory implications: If nothing references the promise or its callbacks, it can be garbage collected; if something holds it (closures, pending awaits), that memory leaks.
Defending against it: Race it against a timeout using Promise.race so a stuck operation rejects instead of hanging.
Q7.What is the difference between the DOM, the Virtual DOM, and the Shadow DOM?
DOM, the Virtual DOM, and the Shadow DOM?They are three unrelated concepts that share the name DOM: the DOM is the real document tree, the Virtual DOM is an in-memory copy used to compute minimal updates, and the Shadow DOM is a scoped, encapsulated subtree.
DOM (Document Object Model):
The browser's live tree of nodes representing the page; mutating it triggers reflow/repaint.
Direct manipulation (e.g. document.createElement) is what everything else ultimately drives.
Virtual DOM:
A lightweight JS object representation kept by libraries like React; not a browser feature.
On state change a new tree is diffed against the old one, and only the differences are applied to the real DOM (reconciliation).
Goal: reduce expensive direct DOM operations, not magically be faster than the DOM.
Shadow DOM:
A real browser feature for Web Components: attaches a hidden subtree via element.attachShadow().
Provides encapsulation: styles and markup inside don't leak out and outside CSS doesn't bleed in.
Key distinction: DOM and Shadow DOM are browser standards; Virtual DOM is a library implementation pattern.
Q8.What is structuredClone and when would you use it over JSON.parse(JSON.stringify())?
structuredClone and when would you use it over JSON.parse(JSON.stringify())?structuredClone() is a built-in that creates a deep copy of a value using the structured clone algorithm, handling many types and circular references that the JSON.parse(JSON.stringify()) trick silently breaks or loses.
What structuredClone handles: Circular references, Date, Map, Set, RegExp, typed arrays, ArrayBuffer, and more.
Where the JSON trick fails:
Throws on circular references.
Drops undefined, functions, and Symbol values.
Converts Date to a string and turns Map/Set into {}.
Limits of both:
Neither clones functions; structuredClone throws on them rather than dropping them.
Class instances lose their prototype (you get a plain object).
Use structuredClone for correct deep copies of complex data; reach for the JSON trick only for simple JSON-safe objects (or older environments).
Q9.Explain the difference between ES Modules (import/export) and CommonJS (require/module.exports). Why did the industry move toward ESM?
import/export) and CommonJS (require/module.exports). Why did the industry move toward ESM?ES Modules are the standardized, statically analyzable module system built into the language, while CommonJS is Node's older runtime-based system. The industry moved to ESM because its static structure enables tree shaking, async loading, and a single module format across browser and server.
CommonJS (require / module.exports):
Synchronous and resolved at runtime; require() can be called conditionally anywhere.
Exports are a dynamic value (a copy of the object reference at require time).
Originated in Node, not native to browsers.
ES Modules (import / export):
Static: imports/exports are resolved at parse time and must sit at the top level.
Bindings are live (an imported value reflects later changes in the exporting module).
Asynchronous loading and native browser support; import() allows dynamic loading when needed.
Why the move to ESM:
Static structure lets bundlers tree-shake dead exports.
One standard format works in browsers and Node, ending the dual-format split.
Better tooling: static analysis, named imports, and clearer dependency graphs.
Q10.Explain the difference between Short Polling, Long Polling, and WebSockets. When is each appropriate?
They are three strategies for getting server data to a client, trading off latency, server load, and complexity: short polling repeatedly asks, long polling waits for an answer, and WebSockets keep a persistent two-way channel open.
Short Polling:
Client sends a request on a fixed interval (e.g. every few seconds) via setInterval + fetch.
Simple, but wasteful: many empty responses, and updates lag up to one interval.
Good for low-frequency, non-urgent data where simplicity matters.
Long Polling:
Client sends a request and the server holds it open until data is available (or a timeout), then the client immediately re-requests.
Lower latency than short polling with fewer empty responses, but each cycle still pays connection overhead.
Good for near-real-time updates when WebSockets aren't available or needed.
WebSockets:
A single persistent, full-duplex TCP connection (upgraded from HTTP) where either side can push at any time.
Lowest latency and overhead for high-frequency, bidirectional traffic, but more infrastructure complexity (stateful connections, scaling, reconnect logic).
Good for chat, live games, collaborative editing, trading dashboards.
Rule of thumb: Infrequent updates: short polling. Timely but mostly one-way: long polling or SSE. Frequent two-way: WebSockets.
Q11.What is the iterable and iterator protocol in JavaScript?
They are two cooperating contracts that let any object define how it is iterated: the iterable protocol says an object can produce an iterator, and the iterator protocol says an object can produce a sequence of values one at a time.
Iterable protocol:
An object is iterable if it implements a method keyed by Symbol.iterator that returns an iterator.
This is what for...of, spread ..., and destructuring consume.
Iterator protocol:
An iterator is an object with a next() method returning { value, done }.
done becomes true when the sequence is exhausted; value holds the current item.
How they connect:
Built-ins like Array, String, Map, and Set already implement both.
Generators (function*) return objects that are both iterable and iterators, which is the easiest way to author custom iteration.
Q12.Explain the Critical Rendering Path. How does JavaScript execution affect the parsing of HTML and CSS?
The Critical Rendering Path is the sequence the browser follows to turn HTML and CSS into pixels: build the DOM, build the CSSOM, combine them into the render tree, lay out, and paint. JavaScript can block and reshape every stage, because by default a script pauses HTML parsing.
The stages:
Parse HTML into the DOM tree.
Parse CSS into the CSSOM tree.
Combine DOM + CSSOM into the render tree (only visible nodes).
Layout (reflow): compute geometry/positions.
Paint and composite to the screen.
How JavaScript affects it:
A plain <script> is parser-blocking: the browser stops building the DOM until the script downloads and runs.
Scripts can read/modify the DOM, so the parser must wait for them to keep state consistent.
CSS is render-blocking and also blocks scripts: a script may query computed styles, so the browser delays script execution until pending CSS (the CSSOM) is ready.
Mitigations:
defer: download in parallel, run after parsing finishes, in order.
async: download in parallel, run as soon as ready (order not guaranteed).
Place non-critical scripts at the end and inline/minimize critical CSS to shorten the path.
Q13.How does the Event Loop decide when to perform a UI re-render in the browser?
The browser tries to render at the display's refresh rate (typically 60fps, so about every 16.7ms), but a render only happens between tasks, once the call stack is empty and after microtasks drain.
Rendering is opportunistic, not guaranteed: The event loop checks for a needed render after each macrotask, but the browser may coalesce or skip frames if nothing visually changed or it's behind.
Order within a frame:
Run a task, drain the microtask queue, then run rendering steps: style, layout, paint, composite.
requestAnimationFrame callbacks fire just before layout/paint, making them the right place for visual updates.
Why long tasks block rendering: Since JS and rendering share one thread, a long-running task or a flood of microtasks delays the next paint and causes jank.
DOM changes don't paint synchronously: they're batched and only flushed at the next rendering opportunity.
Q14.What is the difference between a reflow and a repaint? Which one is more expensive, and how can you minimize them?
A reflow (layout) recalculates element geometry and positions; a repaint redraws pixels without changing layout. Reflow is more expensive because it can cascade across the whole document and always forces a repaint afterward.
Reflow / layout:
Triggered by changes to size, position, or structure (width, adding/removing nodes, font changes).
Can invalidate ancestors and descendants, so it's costly and always followed by a repaint.
Repaint: Triggered by visual-only changes (color, background, visibility) that don't affect geometry.
How to minimize:
Batch DOM reads then writes; avoid interleaving them to prevent layout thrashing.
Don't read layout properties like offsetHeight right after a write, since it forces a synchronous reflow.
Animate cheap, compositor-only properties (transform, opacity) instead of top/left.
Use documentFragment or class toggles to apply many changes at once.
Q15.Explain the difference between microtasks and macrotasks. Which has priority, and why does it matter for UI responsiveness?
Microtasks (promise callbacks, queueMicrotask, MutationObserver) have higher priority than macrotasks (timers, I/O, events): the entire microtask queue is drained after each macrotask and before the next render, which keeps async logic prompt but can also starve rendering if abused.
Macrotasks:
Examples: setTimeout, setInterval, DOM events, network callbacks.
The loop runs exactly one per iteration.
Microtasks:
Examples: .then()/await continuations, queueMicrotask().
After a macrotask, ALL microtasks run, including ones queued during draining.
Why it matters for UI:
Microtasks finish before the next paint, so promise chains resolve before the user sees an update: good for consistency.
But an infinite or runaway microtask loop blocks rendering entirely, since the loop won't paint until the queue empties.
Q16.What happens to the Call Stack when an await keyword is encountered in an async function?
await keyword is encountered in an async function?At await, the async function pauses and its frame is popped off the call stack, returning control to the caller; the rest of the function is scheduled as a microtask to resume once the awaited value settles.
Suspension, not blocking: The stack unwinds back to whatever called the async function, so the thread is free to do other work.
What gets saved: The function's state (local variables, position) is preserved so it can continue exactly where it left off.
Resumption:
When the awaited promise resolves, the continuation is queued as a microtask, then pushed back onto the (empty) stack to run.
Even await of an already-resolved value still defers the continuation to the microtask queue.
Q17.What happens if a microtask recursively schedules another microtask, and what is starvation?
The event loop drains the entire microtask queue before rendering or running the next macrotask, so if a microtask keeps scheduling new microtasks, the loop never gets to exit the microtask phase. That blockage is starvation: rendering, timers, and I/O callbacks are indefinitely delayed.
Microtask draining is exhaustive:
After each macrotask the loop runs ALL queued microtasks, including ones added during draining, before doing anything else.
So a recursively self-scheduling microtask grows the queue as fast as it's emptied.
Starvation:
Lower-priority work (macrotasks like setTimeout, rendering, user input) never runs because the microtask phase never completes.
The page appears frozen even though the CPU is busy.
Contrast with recursive setTimeout: A self-scheduling macrotask yields between turns, letting rendering and other tasks run, so it does not starve the loop.
Fix: defer recursive work to a macrotask (setTimeout) or chunk it so the queue can drain.
Q18.What is a 'Memory Leak' in JavaScript, and how can a closure or an event listener cause one?
closure or an event listener cause one?Q19.How does JavaScript's garbage collection (mark-and-sweep) work?
mark-and-sweep) work?Q20.What are deoptimizations in a JS engine, and what kind of code patterns usually trigger them?
Q21.What is a JavaScript Proxy, and what are some practical use cases for it (e.g., validation, logging)?
Proxy, and what are some practical use cases for it (e.g., validation, logging)?Q22.What are the new features in ES2025/ES2026 that you find most impactful?
ES2025/ES2026 that you find most impactful?Q23.What is Just-In-Time (JIT) compilation in engines like V8?
V8?