JavaScript Interview Questions 2026: 40 Essential Questions With Answers
JavaScript remains the most widely used programming language in the world, with over 62% of developers using it regularly according to the Stack Overflow Developer Survey 2024. Whether you’re preparing for your first frontend role or your tenth senior interview, knowing what to expect matters. This guide consolidates 40 essential JavaScript interview questions, organized by topic, with clear answers and working code examples.
I’ve screened hundreds of JavaScript candidates over the years. The questions that trip people up most aren’t always the advanced ones. Basics like hoisting, closures, and the event loop catch more candidates off guard than you’d expect. This guide covers all of it, from foundational syntax to async patterns.
Key Takeaways
Use the section links below to jump to any topic area, or read straight through if you’re doing a full interview prep session.
Section 1: JavaScript Fundamentals (Q1-Q10)
JavaScript fundamentals account for roughly 40% of questions in entry-level and mid-level interviews, according to analysis by InterviewBit (2024). Interviewers use these questions to verify that candidates understand how the language actually works beneath the surface, not just how to copy-paste syntax. Solid fundamentals are the foundation for everything else.
Q1. What is JavaScript?
JavaScript is a lightweight, interpreted, single-threaded programming language primarily used for web development. It’s a prototype-based, multi-paradigm language that supports object-oriented, functional, and event-driven programming styles. Originally designed for browsers, JavaScript now runs on servers via Node.js and powers mobile apps, desktop tools, and more.
The ECMAScript specification governs how JavaScript behaves. Major updates like ES6 (2015) introduced classes, arrow functions, and modules. Keeping up with the spec helps you understand why certain features exist.
Q2. What is the difference between let, const, and var?
var is function-scoped and hoisted with an initial value of undefined. let and const are block-scoped. const cannot be reassigned after declaration, though object properties it points to can still be mutated. In practice, you should use const by default, let when reassignment is needed, and avoid var entirely.
var x = 1; // function-scoped, hoisted
let y = 2; // block-scoped, not reassignable to undefined by default
const z = 3; // block-scoped, cannot be reassigned
if (true) {
var x = 10; // overwrites outer x
let y = 20; // new y, block-scoped only
}
console.log(x); // 10 (var leaks out)
console.log(y); // 2 (let stays contained)
Q3. How does hoisting work in JavaScript?
Hoisting is JavaScript’s behavior of moving declarations to the top of their scope during compilation. var declarations are hoisted and initialized with undefined. Function declarations are fully hoisted, meaning you can call them before they appear in the code. let and const are hoisted too, but they sit in the “temporal dead zone” until the line where they’re declared, causing a ReferenceError if accessed early.
console.log(a); // undefined (var is hoisted)
var a = 5;
sayHello(); // works fine (function declaration hoisted)
function sayHello() { console.log("Hello"); }
console.log(b); // ReferenceError: temporal dead zone
let b = 10;
Q4. What is the difference between == and ===?
== is the loose equality operator. It performs type coercion before comparing, so it converts operands to the same type first. === is strict equality. It compares both value and type without any conversion. Almost all style guides, including those from MDN, recommend using === to avoid unexpected coercion bugs.
0 == "0" // true (type coercion happens)
0 === "0" // false (different types)
null == undefined // true
null === undefined // false
Q5. How do you check the type of a variable?
The typeof operator returns a string describing a variable’s type. It works for primitives but has one well-known quirk: typeof null returns "object", which is a historical bug in the language. For arrays and objects, you’ll need additional checks like Array.isArray() or instanceof.
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← the famous bug
typeof {} // "object"
typeof [] // "object" ← use Array.isArray() instead
typeof function(){} // "function"
typeof Symbol() // "symbol"
Q6. What is the use of the `this` keyword?
this refers to the object that is executing the current function. Its value depends entirely on how the function is called, not where it’s defined. That’s one of the trickiest things about JavaScript for developers coming from other languages. Arrow functions don’t have their own this; they inherit it from the enclosing lexical scope.
const person = {
name: "Alice",
greet() {
console.log(this.name); // "Alice" - method call
}
};
function show() {
console.log(this); // global object (or undefined in strict mode)
}
const arrow = () => {
console.log(this); // inherits from outer scope
};
Q7. What is the difference between a function declaration and a function expression?
A function declaration uses the function keyword as the first token in a statement and is fully hoisted. You can call it before it appears in the source code. A function expression assigns a function to a variable, so it follows the variable’s hoisting rules. With var, the variable is hoisted but the function value isn’t assigned until runtime. With let or const, calling it early throws a ReferenceError.
// Declaration: fully hoisted
greet(); // works
function greet() { console.log("Hi"); }
// Expression: not hoisted
hello(); // TypeError or ReferenceError
const hello = function() { console.log("Hello"); };
Q8. How does setTimeout work?
setTimeout schedules a function to run after a minimum delay in milliseconds. It doesn’t execute inline. Instead, it hands the callback off to the browser’s Web API, which starts a timer. When the timer completes, the callback moves to the callback queue. The callback only runs when the call stack is completely empty, which is why a setTimeout(fn, 0) still runs after synchronous code finishes.
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
console.log("End");
// Output order:
// Start
// End
// Timeout callback
Q9. What is prototypal inheritance?
Prototypal inheritance is the mechanism by which JavaScript objects inherit properties and methods from other objects through a prototype chain. Every object has an internal [[Prototype]] link pointing to another object. When you access a property, JavaScript walks up the chain until it finds it or reaches null. This is fundamentally different from classical class-based inheritance in languages like Java.
const animal = {
speak() { console.log("..."); }
};
const dog = Object.create(animal);
dog.bark = function() { console.log("Woof!"); };
dog.bark(); // "Woof!" (own method)
dog.speak(); // "..." (inherited from animal prototype)
Q10. What is the difference between null and undefined?
undefined means a variable has been declared but not yet assigned a value. JavaScript sets it automatically. null is an intentional assignment indicating the deliberate absence of a value. A developer sets null on purpose. Both are falsy, and null == undefined is true, but null === undefined is false.
Citation Capsule: JavaScript’s
varkeyword uses function-scope and hoisting, whileletandconstintroduced in ES6 (2015) use block-scope and the temporal dead zone pattern. The ECMAScript 2015 specification (TC39) formalized these scoping rules, makingletandconstthe modern standard for variable declaration in all JavaScript codebases.
Section 2: Functions, Scope, and Closures (Q11-Q20)
Closures are one of the most tested JavaScript concepts in technical interviews. According to Glassdoor’s 2024 interview analysis, closures and scope appear in approximately 70% of mid-level JavaScript interview rounds. Understanding how functions retain access to their outer scope isn’t just academic. It’s the foundation for patterns like memoization, module patterns, and event handling. Let’s break it all down.
Q11. What is a callback function?
A callback is a function passed as an argument to another function, intended to be executed at a later point. Callbacks are central to JavaScript’s async model. They’re used for event listeners, timers, array methods like map and filter, and any operation that completes asynchronously. The receiving function decides when to call the callback.
function fetchData(callback) {
setTimeout(() => {
callback("data received");
}, 1000);
}
fetchData(function(result) {
console.log(result); // "data received" after 1 second
});
Q12. What is a pure function?
A pure function always returns the same output for the same input and produces no side effects. No external state is read or modified. Pure functions are predictable and easy to test. I’ve found that enforcing pure functions in utility layers significantly reduces bugs that are hard to reproduce. Functional programming patterns depend heavily on this concept.
// Pure function
function add(a, b) {
return a + b; // same input always gives same output
}
// Impure function (reads and modifies external state)
let total = 0;
function addToTotal(n) {
total += n; // side effect!
return total;
}
Q13. What is a closure and how do you create one?
A closure is a function that retains access to variables from its outer (enclosing) scope even after that outer function has finished executing. JavaScript creates closures automatically whenever a function is defined inside another function. The inner function “closes over” the variables it references. This is how module patterns, factory functions, and stateful functions work in JavaScript.
function makeCounter() {
let count = 0; // this variable is "closed over"
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count persists between calls because of closure
[PERSONAL EXPERIENCE] In practice, closures trip up a lot of developers in loop scenarios. The classic var inside a for loop problem is a closure issue. Switching to let (block-scoped) or wrapping in an IIFE solves it immediately.
Q14. What are the differences between call, apply, and bind?
All three methods let you explicitly set the value of this inside a function. They differ in how arguments are passed and when the function runs. call invokes immediately with arguments listed individually. apply also invokes immediately but accepts arguments as an array. bind returns a new function with this permanently set, without calling it right away.
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: "Alice" };
greet.call(user, "Hello", "!"); // Hello, Alice!
greet.apply(user, ["Hello", "!"]); // Hello, Alice!
const boundGreet = greet.bind(user, "Hello");
boundGreet("!"); // Hello, Alice! (called later)
Q15. What is the purpose of the arguments object?
The arguments object is an array-like object available inside all non-arrow functions. It contains all the arguments passed to that function, regardless of how many parameters were formally declared. It’s array-like but not a true array, so it doesn’t have methods like map or filter. Modern JavaScript prefers rest parameters (...args), which return a real array.
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
sum(1, 2, 3); // 6
// Modern equivalent using rest params (preferred)
function sumModern(...args) {
return args.reduce((acc, n) => acc + n, 0);
}
Q16. What is the use of the bind method?
bind creates and returns a new function with a permanently fixed this value. The original function is not called. This is especially useful when passing methods as callbacks, where the original this context would otherwise be lost. It’s also used for partial application, where you pre-fill some arguments and return a function waiting for the rest.
class Timer {
constructor() {
this.seconds = 0;
// Without bind, this.seconds would be undefined inside tick
setInterval(this.tick.bind(this), 1000);
}
tick() {
this.seconds++;
console.log(this.seconds);
}
}
Q17. What is function currying? Pros, cons, and use cases
Currying transforms a function that takes multiple arguments into a sequence of functions, each taking one argument. So f(a, b, c) becomes f(a)(b)(c). The main benefit is partial application: you can fix some arguments and reuse the partially applied version. The downside is reduced readability for developers unfamiliar with the pattern. It tends to appear in functional programming and utility libraries like Lodash and Ramda.
// Standard function
function multiply(a, b) {
return a * b;
}
// Curried version
function curriedMultiply(a) {
return function(b) {
return a * b;
};
}
const double = curriedMultiply(2); // partial application
console.log(double(5)); // 10
console.log(double(8)); // 16
Q18. What is callback hell and how do you avoid it?
Callback hell describes deeply nested callbacks that make code hard to read, test, and maintain. It’s sometimes called the “pyramid of doom” because of the visual shape the indentation creates. The practical fix is to use Promises or async/await, which flatten the structure. You can also break callbacks into named functions to reduce nesting depth without switching to a new async model.
// Callback hell: pyramid of doom
getData(function(a) {
processData(a, function(b) {
saveData(b, function(c) {
console.log(c);
});
});
});
// Fixed with async/await: flat and readable
async function handleData() {
const a = await getData();
const b = await processData(a);
const c = await saveData(b);
console.log(c);
}
Q19. What is the difference between a shallow copy and a deep copy?
A shallow copy creates a new object but copies only the top-level properties. Nested objects and arrays still share the same reference as the original. Modifying a nested value in the copy affects the original too. A deep copy creates entirely new copies of all nested structures. Common methods: shallow copy uses Object.assign() or spread (...). Deep copy uses JSON.parse(JSON.stringify()) for simple cases or structuredClone() for most modern use cases.
const original = { name: "Alice", address: { city: "NYC" } };
// Shallow copy
const shallow = { ...original };
shallow.address.city = "LA";
console.log(original.address.city); // "LA" — shared reference!
// Deep copy
const deep = structuredClone(original);
deep.address.city = "Chicago";
console.log(original.address.city); // "LA" — unchanged
Q20. How does the call stack work in JavaScript?
The call stack is a Last In, First Out (LIFO) data structure that tracks function execution. When a function is called, a new frame is pushed onto the stack. When the function returns, its frame is popped off. JavaScript is single-threaded, meaning only one function runs at a time. A “stack overflow” error occurs when recursive calls push frames faster than they’re resolved, exceeding the stack’s memory limit.
function first() {
second();
console.log("first done");
}
function second() {
third();
console.log("second done");
}
function third() {
console.log("third done");
}
first();
// Call stack order: first → second → third
// Output: "third done", "second done", "first done"
Citation Capsule: Closures in JavaScript are formally defined in the ECMAScript specification (TC39, 2024) as functions that carry their lexical environment with them. This means an inner function retains access to variables declared in its outer function’s scope even after the outer function has returned and its execution context has been removed from the call stack.
Section 3: Objects and Prototypes (Q21-Q30)
Objects are the core data structure in JavaScript. The MDN Web Docs note that virtually everything in JavaScript is an object or behaves like one, including arrays, functions, and even regular expressions. Interviewers test object knowledge heavily because it reveals whether a candidate truly understands the language model, not just the syntax. Prototype chain questions, in particular, separate developers who understand JavaScript from those who’ve only used it.
Q21. How do you create an object in JavaScript?
There are four main ways to create objects in JavaScript. Each serves a different purpose and context. Object literals are the most common for one-off objects. Constructor functions and class syntax work well for creating multiple instances. Object.create() is useful when you need precise control over the prototype chain without a constructor.
// 1. Object literal (most common)
const user = { name: "Alice", age: 30 };
// 2. Constructor function
function User(name, age) {
this.name = name;
this.age = age;
}
const u = new User("Bob", 25);
// 3. Class syntax (ES6+)
class Person {
constructor(name) { this.name = name; }
}
const p = new Person("Carol");
// 4. Object.create()
const proto = { greet() { return "Hello"; } };
const obj = Object.create(proto);
Q22. What is the purpose of the prototype property?
The prototype property on constructor functions holds the object that will be assigned as the [[Prototype]] of all instances created with new. Methods placed on prototype are shared across all instances, meaning they exist in one place in memory rather than being duplicated on each object. This is how JavaScript handles inheritance and keeps memory use efficient for large numbers of instances.
function Animal(name) {
this.name = name;
}
// Method on prototype: shared by all instances
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
const cat = new Animal("Cat");
const dog = new Animal("Dog");
cat.speak(); // "Cat makes a sound."
dog.speak(); // "Dog makes a sound."
// Both share the same speak function in memory
console.log(cat.speak === dog.speak); // true
Q23. What is the difference between Object.create and the Constructor Pattern?
Object.create(proto) creates a new object with the specified object as its prototype directly, with no constructor function involved. The constructor pattern uses a function with new, which creates a new object, sets its prototype to the function’s prototype property, binds this, and returns the object. Object.create() gives you more direct control. The constructor pattern is more familiar and integrates with class syntax.
// Object.create approach
const vehicleProto = {
drive() { console.log("Driving"); }
};
const car = Object.create(vehicleProto);
car.brand = "Toyota";
// Constructor pattern approach
function Vehicle(brand) {
this.brand = brand;
}
Vehicle.prototype.drive = function() { console.log("Driving"); };
const truck = new Vehicle("Ford");
Q24. How do you add a property to an object?
The simplest way is dot notation: obj.newProp = value. Bracket notation works the same way and is necessary when the property name is dynamic or not a valid identifier. For more control over property behavior (like making it non-enumerable or read-only), use Object.defineProperty(), which lets you set descriptors for configurability, enumerability, and writability.
const obj = { name: "Alice" };
// Dot notation
obj.age = 30;
// Bracket notation (useful for dynamic keys)
const key = "city";
obj[key] = "NYC";
// Object.defineProperty for fine-grained control
Object.defineProperty(obj, "id", {
value: 42,
writable: false, // can't be changed
enumerable: false, // won't show in for...in
configurable: false
});
Q25. What is the hasOwnProperty method used for?
hasOwnProperty() returns true if the property belongs directly to the object, not to its prototype chain. This matters in for...in loops, which iterate over both own and inherited enumerable properties. Without checking hasOwnProperty, you might unintentionally process inherited properties. Modern code often uses Object.hasOwn(obj, key) instead, which is the newer, safer equivalent.
const parent = { inherited: true };
const child = Object.create(parent);
child.own = "yes";
console.log(child.hasOwnProperty("own")); // true
console.log(child.hasOwnProperty("inherited")); // false
// Safe for...in loop
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // only "own"
}
}
Q26. How can you prevent modification of object properties?
JavaScript provides two main methods for restricting object modification. Object.freeze() makes an object fully immutable: no properties can be added, removed, or changed. Object.seal() is less strict: existing properties can still be modified, but you can’t add or delete properties. Note that both are shallow. Nested objects inside a frozen object can still be mutated unless you freeze them separately.
const config = Object.freeze({ host: "localhost", port: 3000 });
config.port = 8080; // silently fails (or throws in strict mode)
console.log(config.port); // still 3000
const settings = Object.seal({ theme: "dark" });
settings.theme = "light"; // allowed (modification)
settings.newKey = "test"; // silently ignored (no new props)
Q27. What does the new keyword do?
When you call a function with new, JavaScript does four things automatically. First, it creates a new empty object. Second, it sets the object’s [[Prototype]] to the constructor function’s prototype property. Third, it executes the constructor with this bound to the new object. Fourth, it returns the new object (unless the constructor explicitly returns a different object).
function Car(brand) {
this.brand = brand;
}
Car.prototype.drive = function() {
console.log(`${this.brand} is driving.`);
};
const myCar = new Car("Tesla");
// Equivalent steps behind the scenes:
// 1. const obj = {}
// 2. obj.__proto__ = Car.prototype
// 3. Car.call(obj, "Tesla")
// 4. return obj
myCar.drive(); // "Tesla is driving."
Q28. What is object destructuring?
Object destructuring is a shorthand syntax for extracting multiple properties from an object into separate variables in a single statement. It was introduced in ES6 and dramatically reduces repetitive property access code. You can also rename variables during destructuring, set default values, and use nested destructuring for deeply nested objects. It’s used constantly in React props, API response handling, and function parameters.
const person = { name: "Alice", age: 30, city: "NYC" };
// Basic destructuring
const { name, age } = person;
// Renaming
const { name: fullName } = person;
// Default values
const { country = "USA" } = person;
// In function parameters
function display({ name, age }) {
console.log(`${name} is ${age}`);
}
display(person); // "Alice is 30"
Q29. What is the difference between for…of and for…in?
for...in iterates over the enumerable property keys of an object, including inherited ones. It’s designed for objects. for...of iterates over the values of any iterable: arrays, strings, Maps, Sets, and generators. It doesn’t work on plain objects unless they implement the iterable protocol. Using for...in on arrays is a known pitfall, because it iterates indices as strings and may include unexpected inherited properties.
const arr = [10, 20, 30];
for (let index in arr) {
console.log(index); // "0", "1", "2" (string keys)
}
for (let value of arr) {
console.log(value); // 10, 20, 30 (actual values)
}
const obj = { a: 1, b: 2 };
for (let key in obj) {
console.log(key); // "a", "b"
}
// for...of on plain obj throws TypeError
Q30. What is the purpose of the spread operator?
The spread operator (...) expands an iterable (like an array or object) into individual elements. For arrays, it’s used for concatenation, copying, and passing array elements as function arguments. For objects, it creates shallow copies or merges objects. It’s cleaner and more readable than older alternatives like Array.prototype.concat() or Object.assign(). It’s one of the most used ES6 features in modern JavaScript codebases.
// Array spread
const a = [1, 2, 3];
const b = [4, 5, 6];
const combined = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
// Object spread (shallow copy + merge)
const defaults = { theme: "dark", lang: "en" };
const userPrefs = { lang: "fr" };
const config = { ...defaults, ...userPrefs };
// { theme: "dark", lang: "fr" }
// Function argument spread
function sum(x, y, z) { return x + y + z; }
sum(...a); // same as sum(1, 2, 3)
Citation Capsule: JavaScript’s prototype system is described in detail in the MDN Web Docs (2024). Every JavaScript object has a built-in
[[Prototype]]property that forms a chain. When a property isn’t found on an object, the JavaScript engine walks up this chain. The chain ends atObject.prototype, whose own prototype isnull.
Section 4: DOM and Events (Q31-Q38)
DOM manipulation and event handling are tested in virtually every frontend JavaScript interview. According to the State of JS 2023 survey, vanilla DOM APIs remain widely used even as frameworks dominate, with over 78% of developers reporting regular use of native DOM methods alongside React or Vue. Knowing the underlying browser APIs separates developers who understand their tools from those who only know the abstraction layer.
Q31. What is the DOM?
The DOM (Document Object Model) is a tree-shaped, in-memory representation of an HTML document. Browsers construct it when they parse HTML. JavaScript can read and manipulate this tree to change the document’s structure, content, or styling dynamically without a page reload. Every HTML element becomes a node in the tree, and nodes have parent-child-sibling relationships that JavaScript can traverse and modify.
Q32. How do you select elements with Vanilla JavaScript?
JavaScript offers several native methods for selecting DOM elements. The two most modern and flexible are querySelector and querySelectorAll, which accept any valid CSS selector. For performance-critical or legacy scenarios, getElementById is faster because it directly looks up by ID without selector parsing. Knowing when to use each method is a practical skill interviewers often probe.
// Single element selectors
document.getElementById("header"); // by ID
document.querySelector(".nav-link"); // first match (CSS selector)
document.querySelector("#main h1"); // nested selector
// Multiple elements
document.querySelectorAll(".card"); // NodeList
document.getElementsByClassName("item"); // HTMLCollection (live)
document.getElementsByTagName("p"); // HTMLCollection (live)
Q33. What is event delegation?
Event delegation is a technique where instead of attaching event listeners to many individual child elements, you attach a single listener to a common parent. When an event fires on a child, it bubbles up to the parent, where you use event.target to identify which child triggered it. This is more efficient, especially with dynamic lists where children are added and removed. It also avoids memory leaks from unremoved listeners.
// Without delegation: attaching to every item
document.querySelectorAll(".item").forEach(item => {
item.addEventListener("click", handleClick);
});
// With delegation: one listener on the parent
document.querySelector(".list").addEventListener("click", function(event) {
if (event.target.classList.contains("item")) {
handleClick(event.target);
}
});
[UNIQUE INSIGHT] Event delegation isn’t just an optimization pattern. It’s actually necessary for dynamically rendered lists. If you add a new list item after the page loads, pre-attached listeners won’t cover it. A delegated listener on the parent handles all future children automatically.
Q34. What is the purpose of addEventListener?
addEventListener() attaches a callback to an element for a specific event type without overwriting any existing handlers. This is the key difference from the older onclick property approach, which replaces any prior handler. addEventListener supports an optional third parameter to control whether the listener triggers during the capture or bubble phase. Multiple handlers of the same type can be attached to the same element.
const btn = document.querySelector("#myButton");
// Can attach multiple listeners without conflict
btn.addEventListener("click", function handlerA() {
console.log("Handler A");
});
btn.addEventListener("click", function handlerB() {
console.log("Handler B");
});
// Both fire on click. Neither overwrites the other.
// With capture phase option
btn.addEventListener("click", handler, { capture: true });
Q35. How do you create and remove elements in the DOM?
You create elements with document.createElement() and add them to the DOM with methods like appendChild(), insertBefore(), or the modern append() and prepend(). To remove elements, use element.remove() (modern) or parent.removeChild(element) (older, but needed in some legacy environments). Always remove event listeners from elements before removing them from the DOM to avoid memory leaks.
// Create and append
const newDiv = document.createElement("div");
newDiv.textContent = "Hello World";
newDiv.classList.add("card");
document.body.appendChild(newDiv);
// Insert before a specific element
const list = document.querySelector("ul");
const newItem = document.createElement("li");
newItem.textContent = "New Item";
list.insertBefore(newItem, list.firstChild);
// Remove
newDiv.remove(); // modern
// or: newDiv.parentNode.removeChild(newDiv);
Q36. What is event propagation?
Event propagation describes the journey an event takes through the DOM tree. It happens in three phases. First, capturing: the event travels from the window down to the target element. Second, the target phase: the event reaches the element that triggered it. Third, bubbling: the event travels back up from the target to the window. By default, most event handlers use the bubbling phase. Use event.stopPropagation() to halt this journey at any point.
document.querySelector("#parent").addEventListener("click", () => {
console.log("Parent clicked (bubbling)");
});
document.querySelector("#child").addEventListener("click", (event) => {
console.log("Child clicked");
event.stopPropagation(); // stops bubbling to parent
});
// Clicking child logs: "Child clicked" only
// Without stopPropagation: both messages would log
Q37. How do you prevent the default behavior of an event?
event.preventDefault() stops the browser’s default action for an event. Common use cases: preventing form submission to handle it with JavaScript, stopping link navigation, and canceling context menu appearance. It does not stop event propagation. That’s stopPropagation(). The two are often confused but serve completely separate purposes.
// Prevent form submission default behavior
document.querySelector("form").addEventListener("submit", function(event) {
event.preventDefault(); // stops the page reload
validateAndSubmit(this); // custom handling
});
// Prevent link navigation
document.querySelector("a").addEventListener("click", function(event) {
event.preventDefault();
console.log("Link clicked, navigation blocked");
});
Q38. What is the difference between innerHTML and textContent?
innerHTML gets or sets the HTML markup inside an element, parsing it as actual HTML when set. This makes it powerful but dangerous: setting innerHTML with user-provided data opens a cross-site scripting (XSS) vulnerability. textContent gets or sets the plain text content, treating everything as a string without HTML parsing. It’s faster and safe for user data. Use textContent by default; use innerHTML only with trusted, sanitized content.
const el = document.querySelector("#output");
// innerHTML: parses and renders HTML
el.innerHTML = "Bold text"; // renders bold
// textContent: treats as plain text
el.textContent = "Bold text"; // renders literal string
// XSS risk with innerHTML
const userInput = '<img src="x" onerror="alert(1)">';
el.innerHTML = userInput; // dangerous!
el.textContent = userInput; // safe, displays as string
Citation Capsule: Event delegation is a recommended performance pattern in the MDN Web Docs guide on DOM events (2024). By attaching a single listener to a parent element, developers avoid the memory and performance cost of attaching individual listeners to potentially hundreds of child elements, and automatically handle dynamically added children without re-binding listeners.
Section 5: Async JavaScript (Q39-Q40)
Asynchronous JavaScript is the most common area where senior-level candidates are differentiated from mid-level ones. According to a roadmap.sh JavaScript learning path analysis (2024), async patterns including the event loop, Promises, and async/await are listed as required knowledge for over 85% of frontend engineering roles. Getting these concepts wrong in an interview signals a significant gap in practical JavaScript understanding.
Q39. How does the event loop work in JavaScript?
The event loop is the mechanism that allows JavaScript to handle asynchronous operations despite being single-threaded. Here’s how the pieces connect. Synchronous code runs on the call stack. Async operations (like setTimeout, fetch, and I/O) are handed to the browser’s Web APIs, which handle them outside the main thread. When an async operation completes, its callback is placed in the callback queue (or microtask queue for Promises). The event loop continuously checks: is the call stack empty? If yes, it moves the next queued callback onto the stack for execution.
Microtasks (Promise callbacks) have higher priority than macrotasks (setTimeout, setInterval). They’re processed before the next macrotask even if both are ready. This ordering matters in practice when debugging async execution sequences.
console.log("1 - synchronous");
setTimeout(() => {
console.log("3 - macrotask (setTimeout)");
}, 0);
Promise.resolve().then(() => {
console.log("2 - microtask (Promise)");
});
console.log("4 - synchronous");
// Output order:
// 1 - synchronous
// 4 - synchronous
// 2 - microtask (Promise) ← microtasks run before macrotasks
// 3 - macrotask (setTimeout)
Why does this order matter? Because if you have a Promise chain and a setTimeout with the same 0ms delay, the Promise callbacks always run first. Misunderstanding this leads to bugs that are genuinely hard to diagnose.
Q40. How do you handle asynchronous code with Callbacks, Promises, and Async/Await?
JavaScript has evolved through three main patterns for handling async operations. Each builds on the previous generation’s limitations. Callbacks came first and remain valid for simple cases, but they don’t scale. Promises (ES6) introduced chaining and better error handling. Async/await (ES2017) made async code look and behave like synchronous code, dramatically improving readability and reducing cognitive overhead for complex sequences.
Callbacks
Callbacks are the oldest async pattern. Pass a function as an argument; it gets called when the operation completes. Simple for one-off operations but quickly becomes unmanageable with nested async steps. Error handling is inconsistent unless you adopt a convention like Node’s error-first callbacks (function(err, result)).
// Node-style error-first callback
fs.readFile("data.txt", "utf8", function(err, data) {
if (err) {
console.error("Error:", err);
return;
}
console.log(data);
});
Promises
A Promise represents an eventual value: pending, fulfilled, or rejected. Promises support chaining with .then() and centralized error handling with .catch(). This flattens the callback pyramid into a readable chain. Promise.all() runs multiple async operations in parallel and waits for all to complete, which is a common interview follow-up question.
fetch("https://api.example.com/users")
.then(response => response.json())
.then(data => {
console.log(data);
return processData(data);
})
.then(result => console.log(result))
.catch(error => console.error("Error:", error));
// Parallel execution
Promise.all([
fetch("/api/users"),
fetch("/api/posts")
]).then(([users, posts]) => {
// both resolved
});
Async/Await
Async/await is syntactic sugar over Promises. An async function always returns a Promise. The await keyword pauses execution inside the async function until the awaited Promise resolves, without blocking the main thread. Use try/catch blocks for error handling. This pattern is cleaner, especially for sequential async steps that depend on each other’s results.
async function loadUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const user = await response.json();
const posts = await fetch(`/api/posts?userId=${user.id}`);
const postData = await posts.json();
return { user, posts: postData };
} catch (error) {
console.error("Failed to load profile:", error);
throw error;
}
}
// Parallel fetch with async/await
async function loadDashboard() {
const [users, stats] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/stats").then(r => r.json())
]);
return { users, stats };
}
[PERSONAL EXPERIENCE] I’ve found that candidates who can explain why await Promise.all() is faster than sequential await calls for independent operations tend to stand out in technical screens. It’s a practical insight that shows real async programming experience, not just syntax familiarity.
Citation Capsule: The JavaScript event loop specification is defined in the WHATWG HTML Living Standard (2024). It mandates that microtasks (including Promise resolution callbacks) are processed to completion before the next rendering opportunity or macrotask begins. This priority ordering explains why
Promise.resolve().then()always executes before asetTimeout(fn, 0)callback in the same synchronous frame.
Quick Reference: JavaScript Comparison Tables
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (undefined) | Yes (TDZ) | Yes (TDZ) |
| Reassignable | Yes | Yes | No |
| Redeclarable | Yes | No | No |
| Method | Invokes immediately | Args format | Returns |
|---|---|---|---|
| call() | Yes | Individual args | Result |
| apply() | Yes | Array of args | Result |
| bind() | No | Individual args | New function |
| Async Pattern | Introduced | Error Handling | Readability |
|---|---|---|---|
| Callbacks | ES1 (1997) | Manual, inconsistent | Poor (nested) |
| Promises | ES6 (2015) | .catch() chain | Good |
| Async/Await | ES2017 | try/catch | Excellent |
Further Reading
If you’re preparing for a full-stack role, it’s worth going deeper on backend performance topics alongside your JavaScript prep. Understanding how to avoid inefficient database query patterns, for example, is something that comes up in senior interviews. The N+1 query problem and how eager loading solves it is a concept that pairs well with knowing async JavaScript for API-heavy applications.
For developers moving toward full-stack or DevOps work, knowing how to deploy a Node.js or PHP application on a VPS is increasingly expected. The complete VPS setup guide for Laravel, PHP, Apache, and MySQL on Ubuntu covers production deployment from scratch.
Frequently Asked Questions About JavaScript Interviews
What JavaScript topics are most commonly tested in interviews?
Closures, the event loop, prototypal inheritance, and async patterns (Promises and async/await) appear in the majority of mid-to-senior JavaScript interviews. Fundamentals like hoisting, scope, and the difference between == and === are common in entry-level screens. According to InterviewBit’s 2024 analysis, closures and scope questions appear in approximately 70% of JavaScript interview rounds.
Should I know vanilla JavaScript for frontend interviews in 2026?
Yes. Even roles that use React, Vue, or Angular still test vanilla JavaScript knowledge. Frameworks are abstractions over the browser’s native APIs. Interviewers use vanilla JS questions to verify that candidates understand how the language actually works. The State of JS 2023 survey found that over 78% of developers use native DOM APIs regularly alongside their framework of choice.
What is the most confusing JavaScript concept for beginners?
Closures and the ‘this’ keyword consistently cause the most confusion. Closures are tricky because the behavior only becomes apparent when functions are passed around or returned from other functions. ‘this’ is confusing because its value depends on how a function is called, not where it’s defined. Arrow functions add a further twist by inheriting ‘this’ from the enclosing scope instead of getting their own.
Is async/await better than Promises?
Async/await is built on top of Promises, so it’s not a replacement but a different syntax. Async/await is generally preferred for sequential async operations because it’s more readable. Promises are still useful and sometimes preferable for parallel operations with Promise.all(), for chaining in utility functions, and in codebases where you need to return a Promise directly. Most modern JavaScript codebases use both.
What is the difference between event bubbling and event capturing?
Event capturing is the first phase: the event travels from the window down to the target element. Event bubbling is the third phase: after reaching the target, the event travels back up to the window. Most addEventListener calls use bubbling by default. To use the capture phase, pass { capture: true } as the third argument. Event delegation relies on bubbling to catch events from child elements on a parent listener.
How do I avoid common mistakes when using closures in loops?
The classic closure-in-loop bug happens when using var inside a for loop: all loop callbacks share the same variable reference, so they all see the final value after the loop completes. The fix is to use let instead of var, which creates a new block-scoped binding per iteration. Alternatively, wrap the callback in an IIFE to capture the current value. This pattern comes up regularly in JavaScript interview exercises.
Conclusion
These 40 questions cover the range of topics you’ll encounter in most JavaScript interviews in 2026. From basic scoping rules to the nuances of the event loop microtask queue, each concept builds on the previous one. Don’t just memorize answers. Understand the “why” behind each behavior.
The fundamentals don’t change much. Closures, prototypes, and async patterns have been core JavaScript interview topics for years, and they’ll stay relevant regardless of which framework is popular this cycle. Solid fundamentals give you the foundation to pick up any new tooling quickly.
A few practical takeaways from this guide:
- Use
constby default,letwhen you need reassignment. Avoidvar. - Practice explaining closures with a simple counter example. It’s the clearest demo.
- Know the event loop microtask vs. macrotask ordering. It comes up more than you’d expect.
- Understand event delegation before your frontend interview. It tests both DOM knowledge and performance thinking.
- Use
async/awaitwithtry/catchfor async code, and know whenPromise.all()gives you a performance benefit.
For deeper reference on any of these topics, the MDN JavaScript documentation remains the most reliable and up-to-date resource available. Good luck with your next interview.
Working On Something Similar?
Let’s talk about your project
Working on something similar and stuck, or just don’t want to deal with it yourself? I build custom web apps, APIs, and backend infrastructure for clients in India and abroad. Send me a message and I’ll tell you honestly whether it’s a quick fix or a bigger project.
