
JavaScript is an essential skill for anyone pursuing a career in web development, but securing a job in this field can be particularly challenging for newcomers. A critical part of the hiring process is the technical interview, where your JavaScript expertise will be thoroughly evaluated.
To support your preparation and build your confidence, we've put together a list of the top 75+ must-know JavaScript interview questions and answers frequently encountered in interviews.
What's new in the May 2026 update
25 new questions on modern JavaScript: optional chaining, nullish coalescing,Promise.allSettled/Promise.any,AbortController, generators,error.cause, immutable array methods (toSorted/toReversed),Object.groupBy, Setunion/intersection/difference, iterator helpers,structuredClone, private class fields, and more; see the Modern JavaScript (ES2020+) section below.
If you're looking for additional JavaScript interview preparation materials, also check out these resources:
Debouncing is a smart way to handle events that fire repeatedly within a short time, such as typing in a search box or resizing a window. Instead of executing a function every single time the event is triggered, debouncing ensures the function runs only after the event stops firing for a specified time.
It prevents performance bottlenecks by reducing the number of unnecessary function calls, making your app smoother and more efficient.
The debounce method delays a function's execution until after a defined "waiting period" has passed since the last event. Let's see an example using Lodash:
While debouncing waits until user activity stops, throttling ensures the function runs at fixed intervals, regardless of how often the event occurs. Each technique suits specific use cases, such as search boxes (debouncing) versus scroll events (throttling).
Practice implementing a Debounce function on GreatFrontEnd ->
Promise.all()Promise.all() is a powerful method in JavaScript that allows you to handle multiple asynchronous tasks simultaneously. It takes an array of promises and returns a single promise that resolves when all the promises resolve, or rejects if any one of them fails.
This method is perfect when you need to wait for several independent asynchronous tasks to finish before proceeding, like fetching data from multiple APIs.
Here's how Promise.all() works with multiple API requests:
Promise.allPractice implementing a Promise.all function on GreatFrontEnd ->
Deep equality involves comparing two objects or arrays to determine if they are structurally identical. Unlike shallow equality, which only checks if object references are the same, deep equality examines whether all nested values are equal.
Here's a simple deepEqual implementation:
This function uses recursion to check nested properties, ensuring all values match in both objects or arrays. It's a critical concept for comparing complex data structures in frontend development.
Practice implementing Deep Equal on GreatFrontEnd ->
An EventEmitter is a utility that enables objects to listen for and emit events. It implements the observer pattern, allowing you to subscribe to actions or changes and handle them when triggered. This concept is fundamental in both JavaScript and Node.js for managing event-driven programming.
EventEmitter allows flexible communication between components, making it useful in scenarios like state management, logging, or real-time updates.
Practice implementing an Event Emitter on GreatFrontEnd ->
Array.prototype.reduce()?Array.prototype.reduce() is a versatile method for iterating through an array and reducing it to a single value. It processes each element with a callback function, carrying over an accumulator to build the final result. Common use cases include summing numbers, flattening arrays, or even building complex objects.
reduce?Practice implementing Array.protoype.reduce on GreatFrontEnd ->
Flattening transforms a nested array into a single-level array, making it more manageable. Since ES2019, JavaScript provides the Array.prototype.flat() method for this.
Here, .flat(Infinity) ensures the entire array is flattened, no matter how deep. For less deeply nested arrays, you can specify the depth.
Before ES2019, custom solutions were common:
Practice implementing a flatten function on GreatFrontEnd ->
Merging data is crucial when handling complex structures. JavaScript provides efficient ways to combine objects or arrays.
The spread operator is concise and intuitive for merging objects:
Object.assign()Another approach is Object.assign():
Array.concat()For nested objects, you'll need custom logic or libraries:
Alternatively, libraries like Lodash simplify deep merging:
Practice implementing a deep merge function on GreatFrontEnd ->
getElementsByClassNamegetElementsByClassName fetches elements matching a specific class and returns them as a live HTMLCollection.
You can combine class names for more specific selections:
HTMLCollection updates automatically if DOM elements are added or removed.
For more complex selectors, use querySelectorAll:
Practice Using getElementsByClassName on GreatFrontEnd ->
Memoization saves computed results to avoid redundant calculations.
Libraries like Lodash also provide a memoize utility.
Practice implementing a memoize function on GreatFrontEnd ->
getAccessing nested object properties risk errors if any property is undefined. Tools like Lodash's get or JavaScript's optional chaining (?.) help mitigate this.
These methods safely retrieve nested properties without crashing the program.
Practice implementing a get function on GreatFrontEnd ->
Hoisting refers to how JavaScript moves variable and function declarations to the top of their scope during compilation. While only the declaration is hoisted (not the initialization), understanding hoisting helps in writing cleaner and bug-free code.
varVariables declared with var are hoisted and initialized as undefined. Accessing them before initialization results in undefined.
let, const, and classVariables declared with let, const, and class are hoisted but exist in a "temporal dead zone" until their declaration is reached, causing a ReferenceError if accessed early.
Both the declaration and definition of functions are hoisted, allowing them to be called before their declaration.
For function expressions, only the variable is hoisted, not the function itself.
Imports are hoisted, making them available throughout the module. However, their initialization happens before the module code executes.
Modern JavaScript uses let and const to avoid hoisting pitfalls. Declare variables at the top of their scope for better readability and use tools like ESLint to enforce best practices:
By following these practices, you can write robust, maintainable code.
Read more about the concept of "Hoisting" on GreatFrontEnd ->
let, var, and const?In JavaScript, let, var, and const are used to declare variables, but they differ in scope, initialization, redeclaration, reassignment, and behavior when accessed before declaration.
Variables declared with var are function-scoped or global, while let and const are block-scoped (confined to the nearest {} block).
var and let can be declared without initialization, but const requires an initial value.
Variables declared with var can be redeclared, but let and const cannot.
var and let allow reassignment, while const does not.
All variables are hoisted, but var initializes to undefined, whereas let and const exist in a "temporal dead zone" until the declaration is reached.
const for variables that don't change to ensure immutability.let when reassignment is needed.var due to its hoisting and scoping issues.Read more about the differences between let, var, and const on GreatFrontEnd ->
== and === in JavaScript?The == operator checks for equality after performing type conversion, while === checks for strict equality without type conversion.
==)== allows type coercion, which means JavaScript converts values to the same type before comparison. This can lead to unexpected results.
===)=== checks both value and type, avoiding the pitfalls of type coercion.
=== for most comparisons as it avoids implicit type conversion and makes code more predictable.== only when comparing null or undefined for simplicity. -Object.is()Object.is() is similar to === but treats -0 and +0 as distinct and considers NaN equal to itself.
=== for strict comparisons to avoid bugs caused by type coercion.Object.is() for nuanced comparisons like distinguishing -0 and +0.Explore the differences between == and === on GreatFrontEnd ->
The event loop is the backbone of JavaScript's asynchronous behavior, enabling single-threaded execution without blocking.
setTimeout and HTTP requests on separate threadssetTimeout and UI eventsPromise callbacks, executed before macrotasksOutput:
Explanation:
Start, End) run first.Promise 1) follow.Timeout 1, Timeout 2) run last.Explore the event loop in JavaScript on GreatFrontEnd ->
Event delegation is an efficient way to manage events for multiple elements by attaching a single event listener to their common parent.
event.target to determine the clicked element.Explore event delegation in JavaScript on GreatFrontEnd ->
this works in JavaScriptThe value of this depends on how a function is called. Let's explore its different behaviors.
new: When creating objects, this refers to the newly created object.apply, call, or bind: Explicitly sets this to a specified object.this refers to the object the method is called on.window in browsers) or undefined in strict mode.this from their enclosing scope.thisArrow functions simplify usage by capturing this from their lexical scope.
Explore how this works in JavaScript on GreatFrontEnd ->
sessionStorage, and localStorage apart?When it comes to client-side storage, cookies, localStorage, and sessionStorage serve distinct roles:
localStoragesessionStoragelocalStorage (around 5MB).Learn more about cookies, sessionStorage, and localStorage on GreatFrontEnd ->
<script>, <script async>, and <script defer> differ?<script>When using the <script> tag without attributes, it fetches and executes the script immediately, pausing HTML parsing.
Use case: Critical scripts needed before page rendering.
<script async>With async, the script loads in parallel to HTML parsing and executes as soon as it's ready.
Use case: Independent scripts like analytics or ads.
<script defer>When using defer, the script loads alongside HTML parsing but only executes after the HTML is fully parsed.
Use Case: Scripts that rely on a complete DOM structure.
Discover more about <script>, <script async>, and <script defer> on GreatFrontEnd ->
null, undefined?Variables not defined using var, let, or const are considered undeclared and can cause global scope issues.
undefinedA declared variable that hasn't been assigned a value is undefined.
nullRepresents the intentional absence of any value. It's an explicit assignment. Example Code:
Read more about null, undefined, and undeclared variables on GreatFrontEnd ->
.call() vs .apply()?Both .call and .apply let you invoke a function with a specified this value. The key difference lies in how arguments are passed:
.call: Accepts arguments as a comma-separated list..apply: Accepts arguments as an array.Memory aid:
Learn more about .call and .apply on GreatFrontEnd ->
Function.prototype.bind work?The bind method is used to create a new function with a specific this value and, optionally, preset arguments. This ensures that the function always has the correct this context, regardless of how or where it's called.
bind:this is correctly set for the function.Explore Function.prototype.bind on GreatFrontEnd ->
Using arrow functions for methods in constructors automatically binds the this context to the constructor, avoiding the need to manually bind it. This eliminates issues caused by this referring to unexpected contexts.
Arrow functions are particularly useful in React class components, ensuring methods maintain the correct context when passed to child components.
Explore the advantage for using the arrow syntax for a method in a constructor on GreatFrontEnd ->
Prototypal inheritance is a way for objects to share properties and methods through their prototype chain.
null.new to create objects.Explore how prototypal inheritance works on GreatFrontEnd ->
function Person(){}, const person = Person(), and const person = new Person()?function Person(){}: A function declaration, typically used for constructors if written in PascalCase.const person = Person(): Calls the function normally and assigns the result to person. No object creation happens unless explicitly returned.const person = new Person(): Invokes the function as a constructor, creating a new object and setting its prototype to Person.prototype.function foo() {}var foo = function() {}Here are various approaches to creating objects in JavaScript:
{} with key-value pairs.Object constructor: Use the built-in Object constructor with the new keyword.Object.create() method: Create an object with a specific prototype.class syntax for a blueprint-like structure.Explore various ways to create objects in JavaScript on GreatFrontEnd ->
A higher-order function is a function that either:
Explore the definition of a higher-order function on GreatFrontEnd ->
ES5 constructor functions use function constructors and prototypes for object creation and inheritance.
ES2015 Classes use the class keyword for cleaner and more intuitive syntax.
static in ES2015.extends and super keywords in ES2015.Explore differences between ES2015 classes and ES5 constructor functions on GreatFrontEnd ->
Event bubbling is the process where an event triggers on the target element and then propagates upwards through its ancestors in the DOM.
Clicking the child element will log both "Child clicked" and "Parent clicked" due to bubbling.
Use event.stopPropagation() to prevent the event from propagating upwards.
Explore event bubbling on GreatFrontEnd ->
Event capturing, also called "trickling", is the reverse of bubbling. The event propagates from the root element down to the target element.
Capturing is enabled by passing { capture: true } to addEventListener() as the third argument.
Clicking the child will log "Parent capturing" first, followed by "Child clicked".
Explore event capturing on GreatFrontEnd ->
mouseenter and mouseover differ?mouseentermouseoverExplore the differences between mouseenter and mouseover on GreatFrontEnd ->
Explore the difference between synchronous and asynchronous functions on GreatFrontEnd ->
AJAX (Asynchronous JavaScript and XML) is a technique that allows web pages to fetch and send data asynchronously, enabling dynamic updates without reloading the entire page.
XMLHttpRequest; fetch() is the modern alternative.XMLHttpRequest:fetch():Explore AJAX in detail on GreatFrontEnd ->
Explore the advantages and disadvantages of using AJAX on GreatFrontEnd ->
XMLHttpRequest and fetch()?XMLHttpRequestonprogress.onerror event.fetch().catch() for better error management.AbortController for cancellations.fetch() has cleaner syntax and better Promise integration.XMLHttpRequest supports progress tracking, which fetch() does not.Explore the differences between XMLHttpRequest and fetch() on GreatFrontEnd ->
JavaScript features a mix of primitive and non-primitive (reference) data types.
true or false.Tip: Use the typeof operator to determine the type of a variable.
Explore the various data types in JavaScript on GreatFrontEnd ->
JavaScript provides multiple ways to iterate over objects and arrays.
for...inLoops over all enumerable properties, including inherited ones.
Object.keys()Retrieves an array of an object's own enumerable properties.
Object.entries()Returns an array of [key, value] pairs.
Object.getOwnPropertyNames()Includes both enumerable and non-enumerable properties.
for LoopClassic approach for iterating through arrays:
Array.prototype.forEach()Executes a callback for each array item.
for...ofIdeal for looping through iterable objects like arrays.
Array.prototype.entries()Iterates with both index and value.
Explore iteration techniques on GreatFrontEnd ->
...)The spread operator is used to expand elements of arrays or objects.
...)The rest operator collects multiple elements into an array or object.
Explore spread and rest syntax on GreatFrontEnd ->
Mapsize property.Object.keys(), Object.values(), or Object.entries().size property.Explore the difference between Map and plain objects on GreatFrontEnd ->
Map/Set and WeakMap/WeakSetWeakMap and WeakSet keys must be objects, while Map and Set accept any data type.WeakMap and WeakSet allow garbage collection of keys, making them useful for managing memory.Map and Set have a size property.WeakMap and WeakSet are not iterable.Explore the differences between Map/Set and WeakMap/WeakSet on GreatFrontEnd ->
Arrow functions simplify function syntax, making them ideal for inline callbacks.
Explore a use case for the new arrow function syntax on GreatFrontEnd ->
A callback is a function passed as an argument to another function, executed after the completion of an asynchronous task.
Explore the concept of a callback function in asynchronous operations on GreatFrontEnd ->
Debouncing delays execution of a function until a specified time has elapsed since its last invocation.
Throttling ensures a function executes at most once within a set time interval.
Explore the concept of debouncing and throttling on GreatFrontEnd ->
Destructuring simplifies extracting values from arrays or objects into individual variables.
Explore the concept of destructuring assignment on GreatFrontEnd ->
Hoisting moves function declarations to the top of their scope during the compilation phase. However, function expressions and arrow functions do not get hoisted in the same way.
Explore the concept of hoisting on GreatFrontEnd ->
Classes in ES2015 use extends for inheritance and super to access parent constructors and methods.
Explore the concept of inheritance in ES2015 classes on GreatFrontEnd ->
Lexical scoping determines variable access based on where functions are defined, not where they're called.
Explore the concept of lexical scoping on GreatFrontEnd ->
JavaScript has three main types of scope: global, function, and block.
Explore the concept of scope in JavaScript on GreatFrontEnd ->
The spread operator (...) expands elements of an iterable (like arrays) or properties of objects into individual elements.
Explore the spread operator on GreatFrontEnd ->
this work in event handlers?In JavaScript, this in event handlers refers to the element that triggered the event. Its context can be explicitly bound using bind(), arrow functions, or direct assignment.
Explore the concept of this in event handlers on GreatFrontEnd ->
The next 25 questions cover ES2020βES2025 additions that come up in modern JavaScript interviews.
?.) do, and where does it short-circuit?Optional chaining (?.) returns undefined if the value to its left is null or undefined, instead of throwing. It works on property access, function calls, and array indexing.
Important: it short-circuits the rest of the chain the moment any ?. operand is nullish, so user?.a.b.c will short-circuit at user? and never evaluate .a.b.c. It does not protect against 0, '', false, or NaN; those are not nullish.
??) and how does it differ from ||??? returns its right-hand side only when the left side is null or undefined. || returns the right-hand side for any falsy value: 0, '', false, NaN, null, undefined.
Use ?? when "no value provided" is the only case you want to replace. || is right when any falsy value should be treated as "missing", like defaulting a flag.
||=, &&=, ??=)?ES2021's logical assignment operators combine a logical check with assignment, assigning only when the check passes.
a ||= b β assigns b only if a is falsy.a &&= b β assigns b only if a is truthy.a ??= b β assigns b only if a is nullish.They're short-circuiting: the right side runs only when the assignment will happen.
var vs let in a setTimeout loop)var is function-scoped, so all three callbacks close over the same i. By the time the timers fire (after the synchronous loop finishes), i is 3.
Swap var for let:
let creates a fresh binding per iteration, so each callback captures its own i.
Pre-let, the workaround was an IIFE:
Promise.allSettled() differ from Promise.all()?Promise.all() rejects as soon as any input promise rejects, and you lose the results of the others. Promise.allSettled() waits for every promise to settle and returns an array describing each outcome: { status: 'fulfilled', value } or { status: 'rejected', reason }.
Use allSettled when you want every result regardless of failures, e.g., loading multiple independent widgets where one failure shouldn't blank the page.
Explore the difference between Promise.all and Promise.allSettled on GreatFrontEnd ->
Promise.any() and how does it handle rejections?Promise.any() resolves with the value of the first promise to fulfill. It only rejects if all input promises reject, and the rejection is an AggregateError containing every individual reason.
Use it for racing mirrors, fallbacks, or "first response wins" patterns.
fetch request with AbortController?AbortController exposes a signal you pass to any abort-aware API (fetch, addEventListener, streams, observers). Calling .abort() cancels the operation and rejects the associated promise with an AbortError.
Common uses: cancelling in-flight requests when a component unmounts, debouncing search-as-you-type, and timing out long requests. You can also pass the same signal to multiple operations to cancel them as a group.
Explore aborting web requests with AbortController on GreatFrontEnd ->
async/await and raw Promises?async/await is syntactic sugar over Promises. Both run the same machinery; the difference is readability and control flow.
async/await lets you use try/catch, regular if/for, and reads top-to-bottom. Raw Promises shine when you want parallelism (Promise.all) or compose pipelines functionally.
Explore how async/await simplifies asynchronous code on GreatFrontEnd ->
async/await related to them?A generator (function*) is a function that can pause and resume. Each yield suspends execution and returns a value to the caller; next() resumes it.
You can also send values back in via next(value), making generators bidirectional coroutines:
async/await is sugar over generators; an async function is a generator that yields promises, with the runtime calling .next() when each resolves. redux-saga uses generators directly to make async control flow testable.
error.cause and why is it useful?error.cause (ES2022) is a standard way to attach the underlying error when re-throwing, preserving the original error and its stack without string-mashing.
Before cause, the inner error was usually stringified into the outer message (new Error('Failed: ' + e.message)), losing the inner stack. With cause, devtools, console.error, and logging libraries (Sentry, Pino) walk the chain automatically.
Use it at every boundary where you wrap a low-level error into a higher-level one.
toSorted, toReversed, toSpliced, with)?ES2023 added four methods that return a new array instead of mutating the original. They mirror their classic counterparts but are safe with frozen data, React state, or any place where mutation causes bugs.
Before these methods, you had to write [...arr].sort() or arr.slice().reverse() to avoid mutating. They make immutable-style code one call shorter and clearer.
Array.prototype.findLast() do?findLast() (ES2023) returns the last element that matches a predicate. findLastIndex() returns its index. They're the mirror of find() / findIndex().
The pre-2023 alternative was [...arr].reverse().find(...): O(n) extra work and harder to read.
Object.groupBy() / Map.groupBy()?Object.groupBy() and Map.groupBy() (ES2024) group an iterable's items by the return value of a callback. Object.groupBy uses string keys; Map.groupBy allows any value as a key.
They replace the reduce((acc, x) => ...) grouping pattern.
union, intersection, difference)?ES2025 added set-algebra methods to Set. They take any iterable on the right side (not just another Set) and always return a new Set.
Before this, an intersection was new Set([...a].filter(x => b.has(x))). The native methods are also faster, because implementations iterate the smaller side.
.map, .filter, .take on iterators)?ES2025 iterator helpers add .map, .filter, .take, .drop, .flatMap, .reduce, .some, .every, .find, and .toArray directly to iterators (and generators). Unlike array methods, they're lazy, so values flow through one at a time.
Laziness means infinite sequences don't allocate everything, and pipelines don't materialize intermediate arrays.
AsyncIterator.prototype.map and friends do the same over for await...of sources.
Array.fromAsync?Array.fromAsync (ES2024) is the async counterpart of Array.from. It awaits each value from an async iterable (or a sync iterable of promises) and collects them into an array.
Pages are fetched sequentially. For parallel fetching, use Promise.all(urls.map(fetch)) instead. Array.fromAsync fits when each step depends on the previous, or when you just want to materialize an async iterable.
Symbol.iterator?Any object with a [Symbol.iterator]() method becomes iterable and usable with for...of, spread, destructuring, and Array.from. The method must return an iterator (an object with next() returning { value, done }).
The easy way is to delegate to a generator:
For async sources, implement [Symbol.asyncIterator]() instead and consume with for await...of. NodeList, Map, Set, and Node streams all expose themselves through this same protocol.
A shallow copy duplicates the top level only; nested objects are shared by reference. A deep copy recursively duplicates everything.
Pick by use case:
{...obj} / Object.assign: fast, fine when you only mutate the top level.JSON.parse(JSON.stringify(...)): deep but lossy: undefined, functions, Date, Map, Set, RegExp, cycles all break. Avoid in 2026.structuredClone: deep, preserves Date/Map/Set and cycles. Default deep-copy choice.cloneDeep (lodash): handles things structuredClone doesn't (functions, prototypes), but at the cost of bundle size.The classic React bug, setState({ ...state, list: state.list }) followed by mutating state.list, is shallow-copy leakage.
structuredClone() and how is it different from JSON.parse(JSON.stringify(...))?structuredClone() is a built-in that deep-clones a value using the structured clone algorithm. Unlike the JSON round-trip, it handles Date, Map, Set, RegExp, ArrayBuffer, typed arrays, and cyclic references correctly.
It does not clone functions, DOM nodes, or prototypes. For plain-data deep copies, prefer structuredClone over the JSON trick; it's safer and faster.
#field)?Private fields, prefixed with #, are accessible only inside the class that declares them. They're enforced by the language, not a naming convention like _field.
Subclasses can't reach private fields of their parents, and Object.keys() / for...in won't enumerate them. Use them when you need true encapsulation: internal state that consumers (or future subclasses) must not touch.
CommonJS (require / module.exports) is Node.js's legacy module system. ES Modules (import / export) are the standard, supported in browsers and modern Node.js.
Key differences:
await.import from CommonJS, but a CommonJS require of ESM needs await import().New code should default to ESM. Use CommonJS only when targeting old Node or legacy tooling.
Explore the differences between CommonJS and ES Modules on GreatFrontEnd ->
import() and when would you use it?Dynamic import() is a function-like syntax that loads a module at runtime and returns a promise resolving to its namespace object. Unlike static import, it can take a variable specifier and run conditionally.
Common uses:
Most bundlers (webpack, Vite, esbuild) treat dynamic import() as a code-split boundary automatically.
A tagged template literal lets a function process a template string. The tag function receives the static string parts as its first argument and the interpolated values as the rest.
Use them for safe HTML/SQL building, i18n message formatting, GraphQL queries (gql`...`), or styled-components-style CSS-in-JS.
Explore tagged templates on GreatFrontEnd ->
String.prototype.replaceAll() do?replaceAll() (ES2021) replaces every occurrence of a substring or pattern. Before it, replace with a string argument only replaced the first match, forcing a global regex (/.../g) for the common case.
If you pass a regex, it must have the g flag; otherwise replaceAll throws. The function form of the second argument is supported, same as replace.
BigInt and when should you use it?BigInt is a primitive type for integers of arbitrary size, written with an n suffix or via BigInt(...). Standard number loses precision beyond 2^53 - 1 (Number.MAX_SAFE_INTEGER); BigInt does not.
Caveats: you can't mix BigInt and Number in arithmetic (1n + 1 throws), Math.* doesn't accept BigInt, and JSON.stringify throws on BigInt. Reach for it when handling 64-bit IDs, monetary values in minor units, cryptography, or timestamps in nanoseconds: anywhere 2^53 is a real ceiling.
That's the list. The next step is practice: work through each question until you can explain the concept and write the code from scratch without looking.
More JavaScript interview prep:
A broader set of +190 JavaScript interview questions is also available in our GitHub repo.