
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:
import { debounce } from 'lodash';const searchInput = document.getElementById('search-input');const debouncedSearch = debounce(() => {// Perform the search operation hereconsole.log('Searching for:', searchInput.value);}, 300);searchInput.addEventListener('input', debouncedSearch);
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:
const promise1 = fetch('https://api.example.com/data/1');const promise2 = fetch('https://api.example.com/data/2');const promise3 = fetch('https://api.example.com/data/3');Promise.all([promise1, promise2, promise3]).then((responses) => {// Executes only when all promises are resolved.console.log('All responses:', responses);}).catch((error) => {// Catches any error from any promise.console.error('Error:', error);});
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:
function deepEqual(obj1, obj2) {if (obj1 === obj2) return true;if (obj1 == null ||typeof obj1 !== 'object' ||obj2 == null ||typeof obj2 !== 'object')return false;let keys1 = Object.keys(obj1);let keys2 = Object.keys(obj2);if (keys1.length !== keys2.length) return false;for (let key of keys1) {if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) return false;}return true;}// Example usageconst object1 = {name: 'John',age: 30,address: {city: 'New York',zip: '10001',},};const object2 = {name: 'John',age: 30,address: {city: 'New York',zip: '10001',},};console.log(deepEqual(object1, object2)); // true
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.
const eventEmitter = new EventEmitter();// Subscribe to an eventeventEmitter.on('customEvent', (data) => {console.log('Event emitted with data:', data);});// Emit the eventeventEmitter.emit('customEvent', { message: 'Hello, world!' });
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.
const numbers = [1, 2, 3, 4, 5];const sum = numbers.reduce(function (accumulator, currentValue) {return accumulator + currentValue;}, 0);console.log(sum); // Output: 15
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.
const nestedArray = [1, [2, [3, [4, [5]]]]];const flatArray = nestedArray.flat(Infinity);console.log(flatArray); // Output: [1, 2, 3, 4, 5]
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:
// Custom recursive array flattenerfunction flattenArray(arr) {return arr.reduce((acc, val) =>Array.isArray(val) ? acc.concat(flattenArray(val)) : acc.concat(val),[],);}const nestedArray = [1, [2, [3, [4, [5]]]]];const flatArray = flattenArray(nestedArray);console.log(flatArray); // Output: [1, 2, 3, 4, 5]
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:
const obj1 = { a: 1, b: 2 };const obj2 = { b: 3, c: 4 };const mergedObj = { ...obj1, ...obj2 };console.log(mergedObj); // Output: { a: 1, b: 3, c: 4 }
Object.assign()Another approach is Object.assign():
const obj1 = { a: 1, b: 2 };const obj2 = { b: 3, c: 4 };const mergedObj = Object.assign({}, obj1, obj2);console.log(mergedObj); // Output: { a: 1, b: 3, c: 4 }
const array1 = [1, 2, 3];const array2 = [4, 5, 6];const mergedArray = [...array1, ...array2];console.log(mergedArray); // Output: [1, 2, 3, 4, 5, 6]
Array.concat()const array1 = [1, 2, 3];const array2 = [4, 5, 6];const mergedArray = array1.concat(array2);console.log(mergedArray); // Output: [1, 2, 3, 4, 5, 6]
For nested objects, you'll need custom logic or libraries:
function deepMerge(target, source) {for (const key in source) {if (source[key] instanceof Object && key in target) {Object.assign(source[key], deepMerge(target[key], source[key]));}}Object.assign(target || {}, source);return target;}const obj1 = { a: 1, b: { x: 10, y: 20 } };const obj2 = { b: { y: 30, z: 40 }, c: 3 };const mergedObj = deepMerge(obj1, obj2);console.log(mergedObj); // Output: { a: 1, b: { x: 10, y: 30, z: 40 }, c: 3 }
Alternatively, libraries like Lodash simplify deep merging:
const _ = require('lodash');const obj1 = { a: 1, b: { x: 10, y: 20 } };const obj2 = { b: { y: 30, z: 40 }, c: 3 };const mergedObj = _.merge({}, obj1, obj2);console.log(mergedObj); // Output: { a: 1, b: { x: 10, y: 30, z: 40 }, c: 3 }
Practice implementing a deep merge function on GreatFrontEnd ->
getElementsByClassNamegetElementsByClassName fetches elements matching a specific class and returns them as a live HTMLCollection.
// Fetch and loop through elementsconst elements = document.getElementsByClassName('example');for (let i = 0; i < elements.length; i++) {console.log(elements[i].textContent);}
You can combine class names for more specific selections:
const elements = document.getElementsByClassName('class1 class2');
HTMLCollection updates automatically if DOM elements are added or removed.
For more complex selectors, use querySelectorAll:
const elements = document.querySelectorAll('.example');
Practice Using getElementsByClassName on GreatFrontEnd ->
Memoization saves computed results to avoid redundant calculations.
function expensiveOperation(n) {console.log('Calculating for', n);return n * 2;}// Memoize functionfunction memoize(func) {const cache = {};return function (n) {if (cache[n] !== undefined) {console.log('From cache for', n);return cache[n];}const result = func(n);cache[n] = result;return result;};}const memoizedExpensiveOperation = memoize(expensiveOperation);console.log(memoizedExpensiveOperation(5)); // Calculating for 5, 10console.log(memoizedExpensiveOperation(5)); // From cache for 5, 10
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.
const user = { address: { city: 'New York' } };console.log(_.get(user, 'address.city')); // 'New York'console.log(user.address?.city); // 'New York'
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.
console.log(foo); // undefinedvar foo = 1;console.log(foo); // 1
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.
console.log(y); // ReferenceErrorlet y = 'local';
Both the declaration and definition of functions are hoisted, allowing them to be called before their declaration.
foo(); // 'FOOOOO'function foo() {console.log('FOOOOO');}
For function expressions, only the variable is hoisted, not the function itself.
console.log(bar); // undefinedbar(); // TypeError: bar is not a functionvar bar = function () {console.log('BARRRR');};
Imports are hoisted, making them available throughout the module. However, their initialization happens before the module code executes.
foo.doSomething(); // Works fineimport foo from './modules/foo';
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).
if (true) {var foo = 1;let bar = 2;const baz = 3;}console.log(foo); // 1console.log(bar); // ReferenceErrorconsole.log(baz); // ReferenceError
var and let can be declared without initialization, but const requires an initial value.
var a; // Validlet b; // Validconst c; // SyntaxError: Missing initializer
Variables declared with var can be redeclared, but let and const cannot.
var x = 10;var x = 20; // Allowedlet y = 10;let y = 20; // SyntaxError: Identifier 'y' has already been declared
var and let allow reassignment, while const does not.
let a = 1;a = 2; // Allowedconst b = 1;b = 2; // TypeError: Assignment to constant variable
All variables are hoisted, but var initializes to undefined, whereas let and const exist in a "temporal dead zone" until the declaration is reached.
console.log(foo); // undefinedvar foo = 'foo';console.log(bar); // ReferenceErrorlet bar = 'bar';
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.
42 == '42'; // true0 == false; // truenull == undefined; // true
===)=== checks both value and type, avoiding the pitfalls of type coercion.
42 === '42'; // false0 === false; // falsenull === undefined; // false
=== for most comparisons as it avoids implicit type conversion and makes code more predictable.== only when comparing null or undefined for simplicity.let x = null;console.log(x == null); // trueconsole.log(x == undefined); // true
Object.is()Object.is() is similar to === but treats -0 and +0 as distinct and considers NaN equal to itself.
console.log(Object.is(-0, +0)); // falseconsole.log(Object.is(NaN, NaN)); // true
=== 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 macrotasksconsole.log('Start');setTimeout(() => console.log('Timeout 1'), 0);Promise.resolve().then(() => console.log('Promise 1'));setTimeout(() => console.log('Timeout 2'), 0);console.log('End');
Output:
StartEndPromise 1Timeout 1Timeout 2
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.// HTML:// <ul id="item-list">// <li>Item 1</li>// <li>Item 2</li>// </ul>const itemList = document.getElementById('item-list');itemList.addEventListener('click', (event) => {if (event.target.tagName === 'LI') {console.log(`Clicked on ${event.target.textContent}`);}});
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.
Using new: When creating objects, this refers to the newly created object.
function Person(name) {this.name = name;}const person = new Person('Alice');console.log(person.name); // 'Alice'
Using apply, call, or bind: Explicitly sets this to a specified object.
function greet() {console.log(this.name);}const person = { name: 'Alice' };greet.call(person); // 'Alice'
Method call: this refers to the object the method is called on.
const obj = {name: 'Alice',greet() {console.log(this.name);},};obj.greet(); // 'Alice'
Free function call: Defaults to the global object (window in browsers) or undefined in strict mode.
function greet() {console.log(this); // global object or undefined}greet();
Arrow functions: Capture this from their enclosing scope.
const obj = {name: 'Alice',greet: () => {console.log(this.name); // Inherits `this` from enclosing scope},};obj.greet(); // undefined
thisArrow functions simplify usage by capturing this from their lexical scope.
function Timer() {this.seconds = 0;setInterval(() => {this.seconds++;console.log(this.seconds);}, 1000);}const timer = new Timer();
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:
// Set a cookie with an expiry datedocument.cookie = 'userId=12345; expires=Fri, 31 Dec 2025 23:59:59 GMT; path=/';// Read all cookiesconsole.log(document.cookie);// Delete a cookiedocument.cookie = 'userId=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
localStorage// Store data in localStoragelocalStorage.setItem('username', 'john_doe');// Retrieve dataconsole.log(localStorage.getItem('username'));// Remove an itemlocalStorage.removeItem('username');// Clear all localStorage datalocalStorage.clear();
sessionStoragelocalStorage (around 5MB).// Store data in sessionStoragesessionStorage.setItem('sessionId', 'abcdef');// Retrieve dataconsole.log(sessionStorage.getItem('sessionId'));// Remove an itemsessionStorage.removeItem('sessionId');// Clear all sessionStorage datasessionStorage.clear();
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 src="main.js"></script>
<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 async src="analytics.js"></script>
<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.
<script defer src="deferred.js"></script>
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:
let a;console.log(a); // undefinedlet b = null;console.log(b); // nulltry {console.log(c); // ReferenceError: c is not defined} catch (e) {console.log('c is undeclared');}
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:
function sum(a, b) {return a + b;}console.log(sum.call(null, 1, 2)); // 3console.log(sum.apply(null, [1, 2])); // 3
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.const john = {age: 42,getAge: function () {return this.age;},};console.log(john.getAge()); // 42const unboundGetAge = john.getAge;console.log(unboundGetAge()); // undefinedconst boundGetAge = john.getAge.bind(john);console.log(boundGetAge()); // 42const mary = { age: 21 };const boundGetAgeMary = john.getAge.bind(mary);console.log(boundGetAgeMary()); // 21
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.
const Person = function (name) {this.name = name;this.sayName1 = function () {console.log(this.name);};this.sayName2 = () => {console.log(this.name);};};const john = new Person('John');const dave = new Person('Dave');john.sayName1(); // Johnjohn.sayName2(); // Johnjohn.sayName1.call(dave); // Davejohn.sayName2.call(dave); // John
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.function Animal(name) {this.name = name;}Animal.prototype.sayName = function () {console.log(`My name is ${this.name}`);};function Dog(name, breed) {Animal.call(this, name);this.breed = breed;}Dog.prototype = Object.create(Animal.prototype);Dog.prototype.bark = function () {console.log('Woof!');};let fido = new Dog('Fido', 'Labrador');fido.bark(); // "Woof!"fido.sayName(); // "My name is Fido"
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() {}foo(); // "Hello!"function foo() {console.log('Hello!');}
var foo = function() {}foo(); // TypeError: foo is not a functionvar foo = function () {console.log('Hello!');};
Here are various approaches to creating objects in JavaScript:
Object literals: The simplest and most common way to create an object is using curly braces {} with key-value pairs.
const person = {firstName: 'John',lastName: 'Doe',};
Object constructor: Use the built-in Object constructor with the new keyword.
const person = new Object();person.firstName = 'John';person.lastName = 'Doe';
Object.create() method: Create an object with a specific prototype.
const personPrototype = {greet() {console.log(`Hello, my name is ${this.name}.`);},};const person = Object.create(personPrototype);person.name = 'John';person.greet(); // Hello, my name is John.
ES2015 classes: Define objects using the class syntax for a blueprint-like structure.
class Person {constructor(name, age) {this.name = name;this.age = age;}greet() {console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);}}const john = new Person('John', 30);john.greet(); // Hi, I'm John and I'm 30 years old.
Constructor functions: Use a function as a template for creating multiple objects.
function Person(name, age) {this.name = name;this.age = age;}const john = new Person('John', 30);console.log(john.name); // John
Explore various ways to create objects in JavaScript on GreatFrontEnd ->
A higher-order function is a function that either:
Accepts another function as an argument:
function greet(name) {return `Hello, ${name}!`;}function greetUser(greeter, name) {console.log(greeter(name));}greetUser(greet, 'Alice'); // Hello, Alice!
Returns another function:
function multiplier(factor) {return function (num) {return num * factor;};}const double = multiplier(2);console.log(double(4)); // 8
Explore the definition of a higher-order function on GreatFrontEnd ->
ES5 constructor functions use function constructors and prototypes for object creation and inheritance.
function Person(name, age) {this.name = name;this.age = age;}Person.prototype.greet = function () {console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);};const john = new Person('John', 30);john.greet(); // Hi, I'm John and I'm 30 years old.
ES2015 Classes use the class keyword for cleaner and more intuitive syntax.
class Person {constructor(name, age) {this.name = name;this.age = age;}greet() {console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);}}const john = new Person('John', 30);john.greet(); // Hi, I'm John and I'm 30 years old.
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.
const parent = document.getElementById('parent');const child = document.getElementById('child');parent.addEventListener('click', () => {console.log('Parent clicked');});child.addEventListener('click', () => {console.log('Child clicked');});
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.
child.addEventListener('click', (event) => {event.stopPropagation();console.log('Child clicked only');});
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.
const parent = document.getElementById('parent');const child = document.getElementById('child');parent.addEventListener('click',() => {console.log('Parent capturing');},{ capture: true },);child.addEventListener('click', () => {console.log('Child clicked');});
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 ->
const fs = require('fs');const data = fs.readFileSync('file.txt', 'utf8');console.log(data); // Blocks until the file is fully readconsole.log('Program ends');
console.log('Start');fetch('https://api.example.com/data').then((response) => response.json()).then((data) => console.log(data)) // Non-blocking.catch((error) => console.error(error));console.log('End');
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:let xhr = new XMLHttpRequest();xhr.onreadystatechange = function () {if (xhr.readyState === XMLHttpRequest.DONE) {if (xhr.status === 200) {console.log(xhr.responseText);} else {console.error('Request failed');}}};xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);xhr.send();
fetch():fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()).then((data) => console.log(data)).catch((error) => console.error('Fetch error:', error));
Explore AJAX in detail on GreatFrontEnd ->
Explore the advantages and disadvantages of using AJAX on GreatFrontEnd ->
XMLHttpRequest and fetch()?XMLHttpRequestonprogress.onerror event.let xhr = new XMLHttpRequest();xhr.open('GET', 'https://example.com/api', true);xhr.onload = function () {if (xhr.status === 200) {console.log(xhr.responseText);}};xhr.send();
fetch().catch() for better error management.AbortController for cancellations.fetch('https://example.com/api').then((response) => response.json()).then((data) => console.log(data)).catch((error) => console.error(error));
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.
for (const property in obj) {if (Object.hasOwn(obj, property)) {console.log(property);}}
Object.keys()Retrieves an array of an object's own enumerable properties.
Object.keys(obj).forEach((key) => console.log(key));
Object.entries()Returns an array of [key, value] pairs.
Object.entries(obj).forEach(([key, value]) => console.log(`${key}: ${value}`));
Object.getOwnPropertyNames()Includes both enumerable and non-enumerable properties.
Object.getOwnPropertyNames(obj).forEach((prop) => console.log(prop));
for LoopClassic approach for iterating through arrays:
for (let i = 0; i < arr.length; i++) {console.log(arr[i]);}
Array.prototype.forEach()Executes a callback for each array item.
arr.forEach((element, index) => console.log(element, index));
for...ofIdeal for looping through iterable objects like arrays.
for (const element of arr) {console.log(element);}
Array.prototype.entries()Iterates with both index and value.
for (const [index, element] of arr.entries()) {console.log(index, ':', element);}
Explore iteration techniques on GreatFrontEnd ->
...)The spread operator is used to expand elements of arrays or objects.
Copying arrays/objects:
const array = [1, 2, 3];const newArray = [...array]; // [1, 2, 3]
Merging arrays/objects:
const arr1 = [1, 2];const arr2 = [3, 4];const mergedArray = [...arr1, ...arr2]; // [1, 2, 3, 4]
Passing function arguments:
const nums = [1, 2, 3];console.log(Math.max(...nums)); // 3
...)The rest operator collects multiple elements into an array or object.
Function parameters:
function sum(...numbers) {return numbers.reduce((a, b) => a + b);}sum(1, 2, 3); // 6
Destructuring:
const [first, ...rest] = [1, 2, 3];console.log(rest); // [2, 3]
Explore spread and rest syntax on GreatFrontEnd ->
Mapsize property.const map = new Map();map.set('key', 'value');console.log(map.size); // 1
Object.keys(), Object.values(), or Object.entries().size property.const obj = { key: 'value' };console.log(Object.keys(obj).length); // 1
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.// Map Exampleconst map = new Map();map.set({}, 'value');console.log(map.size); // 1// WeakMap Exampleconst weakMap = new WeakMap();let obj = {};weakMap.set(obj, 'value');obj = null; // Key is garbage-collected
Explore the differences between Map/Set and WeakMap/WeakSet on GreatFrontEnd ->
Arrow functions simplify function syntax, making them ideal for inline callbacks.
// Traditional function syntaxconst numbers = [1, 2, 3, 4, 5];const doubledNumbers = numbers.map(function (number) {return number * 2;});console.log(doubledNumbers); // [2, 4, 6, 8, 10]// Arrow function syntaxconst doubledWithArrow = numbers.map((number) => number * 2);console.log(doubledWithArrow); // [2, 4, 6, 8, 10]
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.
function fetchData(callback) {setTimeout(() => {const data = { name: 'John', age: 30 };callback(data);}, 1000);}fetchData((data) => {console.log(data); // { name: 'John', age: 30 }});
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.
function debounce(func, delay) {let timeoutId;return (...args) => {clearTimeout(timeoutId);timeoutId = setTimeout(() => func.apply(this, args), delay);};}
Throttling ensures a function executes at most once within a set time interval.
function throttle(func, limit) {let inThrottle;return (...args) => {if (!inThrottle) {func.apply(this, args);inThrottle = true;setTimeout(() => (inThrottle = false), limit);}};}
Explore the concept of debouncing and throttling on GreatFrontEnd ->
Destructuring simplifies extracting values from arrays or objects into individual variables.
// Array destructuringconst [a, b] = [1, 2];// Object destructuringconst { name, age } = { name: 'John', age: 30 };
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.
// Function declarationhoistedFunction(); // Works finefunction hoistedFunction() {console.log('This function is hoisted');}// Function expressionnonHoistedFunction(); // Throws an errorvar nonHoistedFunction = function () {console.log('This function is not hoisted');};
Explore the concept of hoisting on GreatFrontEnd ->
Classes in ES2015 use extends for inheritance and super to access parent constructors and methods.
class Animal {constructor(name) {this.name = name;}speak() {console.log(`${this.name} makes a noise.`);}}class Dog extends Animal {constructor(name, breed) {super(name);this.breed = breed;}speak() {console.log(`${this.name} barks.`);}}const dog = new Dog('Rex', 'German Shepherd');dog.speak(); // Rex barks.
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.
function outerFunction() {let outerVariable = 'I am outside!';function innerFunction() {console.log(outerVariable); // I am outside!}innerFunction();}outerFunction();
Explore the concept of lexical scoping on GreatFrontEnd ->
JavaScript has three main types of scope: global, function, and block.
// Global scopevar globalVar = 'I am global';function myFunction() {// Function scopevar functionVar = 'I am in a function';if (true) {// Block scopelet blockVar = 'I am in a block';console.log(blockVar); // Accessible here}// console.log(blockVar); // Error}
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.
// Copying an arrayconst arr1 = [1, 2, 3];const arr2 = [...arr1];// Merging arraysconst mergedArray = [...arr1, [4, 5]];// Copying an objectconst obj1 = { a: 1, b: 2 };const obj2 = { ...obj1 };// Passing as function argumentsconst sum = (x, y, z) => x + y + z;const nums = [1, 2, 3];sum(...nums); // 6
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.
const button = document.querySelector('button');button.addEventListener('click', function () {console.log(this); // Refers to the button});const obj = {handleClick: function () {console.log(this); // Refers to obj},};button.addEventListener('click', obj.handleClick.bind(obj));
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.
const user = { profile: null };console.log(user.profile?.name); // undefined — no throwconsole.log(user.callbacks?.onSave?.()); // undefinedconsole.log(user.tags?.[0]); // undefined
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.
const port = 0;console.log(port || 3000); // 3000 — 0 is falsyconsole.log(port ?? 3000); // 0 — 0 is not nullishconst name = '';console.log(name || 'Anonymous'); // 'Anonymous'console.log(name ?? 'Anonymous'); // ''
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.
const config = { retries: 0, host: '' };config.host ||= 'localhost'; // assigns — '' is falsyconfig.retries ??= 3; // does NOT assign — 0 is not nullishconfig.debug &&= 'verbose'; // does NOT assign — debug is undefined
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)for (var i = 0; i < 3; i++) {setTimeout(() => console.log(i), 0);}// 3, 3, 3
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:
for (let i = 0; i < 3; i++) {setTimeout(() => console.log(i), 0);}// 0, 1, 2
let creates a fresh binding per iteration, so each callback captures its own i.
Pre-let, the workaround was an IIFE:
for (var i = 0; i < 3; i++) {(function (j) {setTimeout(() => console.log(j), 0);})(i);}
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 }.
const results = await Promise.allSettled([fetch('/api/a'),fetch('/api/b'),fetch('/api/c'),]);for (const result of results) {if (result.status === 'fulfilled') {console.log('ok:', result.value);} else {console.error('failed:', result.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.
try {const fastest = await Promise.any([fetch('https://mirror-1.example.com/data'),fetch('https://mirror-2.example.com/data'),fetch('https://mirror-3.example.com/data'),]);console.log('first to respond:', fastest);} catch (err) {console.error(err.errors); // array of all rejection reasons}
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.
const controller = new AbortController();const promise = fetch('/api/slow', { signal: controller.signal });// Cancel after 5 secondssetTimeout(() => controller.abort(), 5000);try {const res = await promise;console.log(await res.json());} catch (err) {if (err.name === 'AbortError') {console.log('Request cancelled');}}
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.
// Raw promise chainfunction loadUser(id) {return fetch(`/users/${id}`).then((res) => res.json()).then((user) => fetch(`/orgs/${user.orgId}`)).then((res) => res.json());}// async/await equivalentasync function loadUser(id) {const user = await (await fetch(`/users/${id}`)).json();const org = await (await fetch(`/orgs/${user.orgId}`)).json();return org;}
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.
function* counter() {yield 1;yield 2;yield 3;}const c = counter();console.log(c.next()); // { value: 1, done: false }console.log(c.next()); // { value: 2, done: false }console.log(c.next()); // { value: 3, done: true }
You can also send values back in via next(value), making generators bidirectional coroutines:
function* dialog() {const name = yield 'What is your name?';yield `Hello, ${name}!`;}const d = dialog();d.next(); // { value: 'What is your name?', done: false }d.next('Ada'); // { value: 'Hello, Ada!', done: false }
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.
async function loadUser(id) {try {return await fetchUser(id);} catch (originalError) {throw new Error(`Failed to load user ${id}`, { cause: originalError });}}try {await loadUser(42);} catch (err) {console.error(err.message); // 'Failed to load user 42'console.error(err.cause); // the original network error, with full stack}
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.
const arr = [3, 1, 2];const sorted = arr.toSorted(); // [1, 2, 3] — arr unchangedconst reversed = arr.toReversed(); // [2, 1, 3] — arr unchangedconst spliced = arr.toSpliced(1, 1, 9, 9); // [3, 9, 9, 2]const replaced = arr.with(0, 99); // [99, 1, 2]console.log(arr); // [3, 1, 2] — still original
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().
const events = [{ type: 'click', t: 100 },{ type: 'scroll', t: 200 },{ type: 'click', t: 300 },];const lastClick = events.findLast((e) => e.type === 'click');console.log(lastClick); // { type: 'click', t: 300 }
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.
const people = [{ name: 'Ada', team: 'eng' },{ name: 'Lin', team: 'design' },{ name: 'Rao', team: 'eng' },];const byTeam = Object.groupBy(people, (p) => p.team);// { eng: [{Ada}, {Rao}], design: [{Lin}] }const teamObj = { id: 1 };const byRef = Map.groupBy(people, (p) => (p.team === 'eng' ? teamObj : null));// Map { teamObj => [{Ada}, {Rao}], null => [{Lin}] }
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.
const a = new Set([1, 2, 3]);const b = new Set([3, 4, 5]);a.union(b); // Set { 1, 2, 3, 4, 5 }a.intersection(b); // Set { 3 }a.difference(b); // Set { 1, 2 }a.symmetricDifference(b); // Set { 1, 2, 4, 5 }a.isSubsetOf(b); // falsea.isSupersetOf(b); // falsea.isDisjointFrom(b); // false
Before this, an intersection was new Set([...a].filter(x => b.has(x))). The native methods are also faster — 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 — values flow through one at a time.
function* naturals() {let n = 1;while (true) yield n++;}const firstFiveSquares = naturals().map((n) => n * n).take(5).toArray();console.log(firstFiveSquares); // [1, 4, 9, 16, 25]
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.
async function* fetchPages(urls) {for (const url of urls) {const res = await fetch(url);yield await res.json();}}const allPages = await Array.fromAsync(fetchPages(['/api/p/1', '/api/p/2', '/api/p/3']),);
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 — 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:
class Range {constructor(from, to) {this.from = from;this.to = to;}*[Symbol.iterator]() {for (let n = this.from; n <= this.to; n++) yield n;}}const r = new Range(1, 4);for (const n of r) console.log(n); // 1 2 3 4console.log([...r]); // [1, 2, 3, 4]const [first, ...rest] = r; // 1, [2, 3, 4]
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.
const original = { user: { name: 'Ada' }, tags: ['x'] };// Shallow copies — nested refs sharedconst a = { ...original };const b = Object.assign({}, original);a.user.name = 'Lin';console.log(original.user.name); // 'Lin' — mutation leaked// Deep copiesconst c = JSON.parse(JSON.stringify(original)); // lossyconst d = structuredClone(original); // preferred
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) — 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.
const original = {date: new Date(),map: new Map([['k', 'v']]),set: new Set([1, 2, 3]),};original.self = original; // cycleconst copy = structuredClone(original);console.log(copy.date instanceof Date); // trueconsole.log(copy.map instanceof Map); // trueconsole.log(copy.self === copy); // true — cycle preserved
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.
class Counter {#count = 0;increment() {this.#count++;}get value() {return this.#count;}}const c = new Counter();c.increment();console.log(c.value); // 1console.log(c.#count); // SyntaxError
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.
// CommonJSconst fs = require('fs');module.exports = { foo: 1 };// ES Modulesimport fs from 'node:fs';export const foo = 1;
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.
button.addEventListener('click', async () => {const { default: Chart } = await import('./Chart.js');new Chart(document.getElementById('canvas')).render();});
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.
function html(strings, ...values) {return strings.reduce((out, str, i) => {const safe = values[i] != null ? escapeHtml(values[i]) : '';return out + str + safe;}, '');}const name = '<script>alert(1)</script>';const out = html`<p>Hello, ${name}!</p>`;// "<p>Hello, <script>alert(1)</script>!</p>"
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.
const s = 'cat, cat, cat';console.log(s.replace('cat', 'dog')); // 'dog, cat, cat'console.log(s.replaceAll('cat', 'dog')); // 'dog, dog, dog'console.log(s.replaceAll(/CAT/gi, 'dog')); // 'dog, dog, dog'
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.
const big = 9007199254740993n;console.log(big + 1n); // 9007199254740994nconsole.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 — wrong
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.