
If you have 5+ years of JavaScript under your belt, the bar in interviews shifts. You're expected to know the language well, but what gets you hired is judgment — knowing which tradeoffs matter in production, where the language gets weird, and how the framework you use is built on top of it.
This article covers 20 questions that come up in senior and lead engineer loops at large tech companies, plus three open-ended scenarios that test depth rather than recall.
If you're looking for additional JavaScript interview preparation materials, also check out these resources:
Three open-ended scenarios that come up in senior loops. Unlike recall questions, there's no single right answer — what's being evaluated is how you decompose the problem.
"Your team's dashboard runs in a browser tab for 6+ hours per day. Users report it grows to ~1 GB of memory usage. Walk me through your debugging approach."
A solid answer walks through the layers:
Detached HTMLDivElement points to DOM nodes still referenced from JavaScript after they were removed from the tree (typical cause: an event listener on window/document that captured the node in a closure and was never removed). A growing count of plain Objects often points to an unbounded cache or Map that nothing evicts.setInterval not cleared on unmount, addEventListener on window/document without cleanup, observers (IntersectionObserver, MutationObserver, ResizeObserver) not disconnected, subscriptions to external stores not unsubscribed.What separates a strong answer is naming specific tools and constructor names, rather than just listing "common causes of memory leaks".
"Write the minimum-latency code for this dependency graph. Why might
Promise.allbe the wrong tool?"
async function loadCheckout(userId) {// Step 1 must complete before Step 3 because Step 3 needs the cart id.const cart = await getCart(userId);// Steps 2 and 3 are independent and can run concurrently.const [user, lineItems] = await Promise.all([getUser(userId),getCartItems(cart.id),]);return { cart, user, lineItems };}
Wrapping all three in Promise.all doesn't work — getCartItems needs cart.id, which isn't known until getCart resolves. Promise.all only parallelizes inputs that are already independent. When there's a dependency chain, await the dependency first, then Promise.all the leaves. (For a deeper comparison, see Promise.all vs Promise.allSettled.)
setInterval(updateUI, 16) for an animation — what's wrong with this?Three issues:
setInterval doesn't synchronize with paint. You'll drift in and out of phase with the refresh cycle, getting visible stutter. requestAnimationFrame schedules work to run right before the next paint.setInterval is throttled in background tabs (Chrome drops to ~1 Hz, then to ~1/min under "intensive throttling" after 5 minutes hidden), but it still fires. requestAnimationFrame simply pauses while the tab is hidden — saving CPU and battery.setInterval will fire as soon as the previous one finishes, with no breathing room. rAF naturally skips a frame and resumes on the next paint.For animations driven from JS, requestAnimationFrame is the default. For purely declarative timing (e.g. fading in an element from 0 to 1 opacity over 300 ms), the Web Animations API or CSS transitions are usually a better fit since they run off the main thread.
For the questions below, what separates a passing answer from a strong one is rarely correctness — it's the depth of the connection drawn between the language feature and its real-world consequence:
| Question | Surface answer | Deeper answer |
|---|---|---|
| "What is a closure?" | "A function that remembers its scope" | "Functions retain access to their lexical environment after the outer function returns. This is what makes React hooks work — each useState call closes over a specific slot on the fiber. The flip side is that closures pin their captured variables in memory, which is why stale-closure bugs in useEffect happen when the deps array is wrong." |
| "How do Promises work?" | "They have .then and .catch" | "Promises are state machines: pending → fulfilled or rejected, with handlers scheduled as microtasks (so they run before the next macrotask but after the current synchronous block). await desugars to .then, which is why await in a loop runs sequentially while Promise.all runs concurrently. Unhandled rejections fire an unhandledrejection event you can listen for globally to catch missing .catch paths." |
"What does this refer to?" | "The object that called the function" | "It depends on the call site, not where the function was defined — for a regular function it's the receiver, for arrow functions it's the lexical this from the enclosing scope, for new it's the new instance, and in strict mode it's undefined for an unbound call. Class fields with arrow functions are commonly used specifically to avoid this getting lost when a method is passed as a callback." |
The deeper answer in each case ties the language feature to a specific failure mode and, where it applies, to where the consequence shows up in a framework like React.
Anonymous functions provide a concise way to define functions, especially useful for simple operations or callbacks. They are commonly used in:
map(), filter(), and reduce().Some examples:
// Encapsulating Code using IIFE(function () {// Some code here.})();// CallbackssetTimeout(function () {console.log('Hello world!');}, 1000);// Functional programming constructsconst arr = [1, 2, 3];const double = arr.map(function (el) {return el * 2;});console.log(double); // [2, 4, 6]
Explore a typical use case for anonymous functions in JavaScript on GreatFrontEnd
A closure is a function that retains access to variables from its enclosing scope even after the outer function has finished executing. The function effectively carries its original environment with it.
function outerFunction() {const outerVar = 'I am outside of innerFunction';function innerFunction() {console.log(outerVar); // `innerFunction` can still access `outerVar`.}return innerFunction;}const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`.inner(); // "I am outside of innerFunction"// Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`.
Closures are useful for:
Explore what a closure is in JavaScript, and why you would use one on GreatFrontEnd
Pros:
Avoid callback hell: Promises simplify nested callbacks.
// Callback hellgetData1((data) => {getData2(data, (data) => {getData3(data, (result) => {console.log(result);});});});
Sequential code: Easier to write and read using .then().
Parallel code: Simplifies managing multiple promises with Promise.all().
Promise.all([getData1(), getData2(), getData3()]).then((results) => {console.log(results);}).catch((error) => {console.error('Error:', error);});
Cons:
Explore the pros and cons of using Promises instead of callbacks in JavaScript on GreatFrontEnd
AbortController in JavaScript?AbortController allows you to cancel ongoing asynchronous operations like fetch requests. To use it:
const controller = new AbortController();
2.Pass the signal: Add the signal to the fetch request options.controller.abort() to cancel the request.Here is an example of how to use AbortControllers with the fetch() API:
const controller = new AbortController();const signal = controller.signal;fetch('YOUR API', { signal }).then((response) => {// Handle response}).catch((error) => {if (error.name === 'AbortError') {console.log('Request aborted');} else {console.error('Error:', error);}});// Call abort() to abort the requestcontroller.abort();
Some of its use cases can be:
fetch() request on a user actionExplore how to abort a web request using AbortController in JavaScript on GreatFrontEnd
In JavaScript it's very easy to extend a built-in/native object. You can simply extend a built-in object by adding properties and functions to its prototype.
String.prototype.reverseString = function () {return this.split('').reverse().join('');};console.log('hello world'.reverseString()); // Outputs 'dlrow olleh'// Instead of extending the built-in object, write a pure utility function to do it.function reverseString(str) {return str.split('').reverse().join('');}console.log(reverseString('hello world')); // Outputs 'dlrow olleh'
While this may seem like a good idea at first, it is dangerous in practice. Imagine your code uses a few libraries that both extend the Array.prototype by adding the same contains method, the implementations will overwrite each other and your code will have unpredictable behavior if these two methods do not work the same way.
Extending built-in objects can lead to issues such as:
Explore why extending built-in JavaScript objects is not a good idea on GreatFrontEnd
In the browser, the global scope refers to the top-level context where variables, functions, and objects are accessible throughout the code. This scope is represented by the window object. Variables and functions declared outside of any function or block (excluding modules) are added to the window object, making them globally accessible.
For example:
// This runs in the global scope, not within a module.let globalVar = 'Hello, world!';function greet() {console.log('Greetings from the global scope!');}console.log(window.globalVar); // 'Hello, world!'window.greet(); // 'Greetings from the global scope!'
In this example, globalVar and greet are attached to the window object and can be accessed from anywhere in the global scope.
Generally, it's advisable to avoid polluting the global namespace unless necessary. Key reasons include:
In JavaScript, modules are reusable pieces of code that encapsulate functionality, making it easier to manage, maintain, and structure your applications. Modules allow you to break down your code into smaller, manageable parts, each with its own scope.
CommonJS is an older module system that was initially designed for server-side JavaScript development with Node.js. It uses the require() function to load modules and the module.exports or exports object to define the exports of a module.
// my-module.jsconst value = 42;module.exports = { value };// main.jsconst myModule = require('./my-module.js');console.log(myModule.value); // 42
ES Modules (ECMAScript Modules) are the standardized module system introduced in ES6 (ECMAScript 2015). They use the import and export statements to handle module dependencies.
// my-module.jsexport const value = 42;// main.jsimport { value } from './my-module.js';console.log(value); // 42
Explore the differences between CommonJS modules and ES modules in JavaScript on GreatFrontEnd
Immutability is a core principle in functional programming but it has lots to offer to object-oriented programs as well.
Mutable objects in JavaScript allow for modifications to their properties and values after creation. This behavior is default for most objects.
let mutableObject = {name: 'John',age: 30,};// Modify the objectmutableObject.name = 'Jane';console.log(mutableObject); // Output: { name: 'Jane', age: 30 }
Mutable objects like mutableObject above can have their properties changed directly, making them flexible for dynamic updates.
In contrast, immutable objects cannot be modified once created. Any attempt to change their content results in the creation of a new object with the updated values.
const immutableObject = Object.freeze({name: 'John',age: 30,});// Attempting to modify the objectimmutableObject.name = 'Jane'; // This change won't affect the objectconsole.log(immutableObject); // Output: { name: 'John', age: 30 }
Here, immutableObject remains unchanged after creation due to Object.freeze(), which prevents modifications to its properties.
The primary difference lies in modifiability. Mutable objects allow changes to their properties directly, while immutable objects ensure the integrity of their initial state by disallowing direct modifications.
const vs immutable objectsA common confusion is that declaring a variable using const makes the value immutable, which is not true at all.
Using const prevents the reassignment of variables but doesn't make non-primitive values immutable.
// Using constconst person = { name: 'John' };person = { name: 'Jane' }; // Error: Assignment to constant variableperson.name = 'Jane'; // Allowed, person.name is now 'Jane'// Using Object.freeze() to create an immutable objectconst frozenPerson = Object.freeze({ name: 'John' });frozenPerson.name = 'Jane'; // Silently ignored in sloppy mode; throws TypeError in strict modefrozenPerson = { name: 'Jane' }; // Error: Assignment to constant variable
In the first example with const, reassigning a new object to person is not allowed, but modifying the name property is permitted. In the second example, Object.freeze() makes the frozenPerson object immutable, preventing any changes to its properties.
Explore the difference between mutable and immutable objects in JavaScript on GreatFrontEnd
Static class members in JavaScript, denoted by the static keyword, are accessed directly on the class itself, not on instances. They serve multiple purposes:
class Config {static API_KEY = 'your-api-key';static FEATURE_FLAG = true;}console.log(Config.API_KEY); // Output: 'your-api-key'console.log(Config.FEATURE_FLAG); // Output: true
class Arithmetic {static add(a, b) {return a + b;}static subtract(a, b) {return a - b;}}console.log(Arithmetic.add(2, 3)); // Output: 5console.log(Arithmetic.subtract(5, 2)); // Output: 3
class Singleton {static instance;static getInstance() {if (!this.instance) {this.instance = new Singleton();}return this.instance;}}const singleton1 = Singleton.getInstance();const singleton2 = Singleton.getInstance();console.log(singleton1 === singleton2); // Output: true
Explore why you might want to create static class members in JavaScript on GreatFrontEnd
Symbols used for in JavaScript?Symbols in JavaScript, introduced in ES6, are unique and immutable identifiers primarily used as object property keys to avoid name collisions. They can be created using the Symbol() function, ensuring each Symbol value is unique even if descriptions are identical. Symbol-keyed properties are skipped by for...in, Object.keys(), Object.getOwnPropertyNames(), and JSON.stringify(), which makes them useful as quasi-private property keys (they're still retrievable via Object.getOwnPropertySymbols() and Reflect.ownKeys()).
const sym1 = Symbol();const sym2 = Symbol('uniqueKey');console.log(typeof sym1); // "symbol"console.log(sym1 === sym2); // false, each symbol is uniqueconst obj = {};const sym = Symbol('uniqueKey');obj[sym] = 'value';console.log(obj[sym]); // "value"
Key characteristics include:
for...in, Object.keys(), Object.getOwnPropertyNames(), or JSON.stringify() — but they're still accessible via Object.getOwnPropertySymbols().Global Symbols can be created using Symbol.for('key'), allowing reuse across different parts of codebases:
const globalSym1 = Symbol.for('globalKey');const globalSym2 = Symbol.for('globalKey');console.log(globalSym1 === globalSym2); // trueconst key = Symbol.keyFor(globalSym1);console.log(key); // "globalKey"
There are some well known Symbol in JavaScript like:
Symbol.iterator: Defines the default iterator for an object.Symbol.toStringTag: Used to create a string description for an object.Symbol.hasInstance: Used to determine if an object is an instance of a constructor.Explore what Symbols are used for in JavaScript on GreatFrontEnd
JavaScript object getters and setters are essential for controlling access to object properties, offering customization when getting or setting values.
const user = {_firstName: 'John',_lastName: 'Doe',get fullName() {return `${this._firstName} ${this._lastName}`;},set fullName(value) {const parts = value.split(' ');this._firstName = parts[0];this._lastName = parts[1];},};console.log(user.fullName); // Output: 'John Doe'user.fullName = 'Jane Smith';console.log(user.fullName); // Output: 'Jane Smith'
Getters (fullName) compute values based on internal properties (_firstName and _lastName), while setters (fullName) update these properties based on assigned values ('Jane Smith'). These mechanisms enhance data encapsulation and allow for custom data handling in JavaScript objects.
Explore what JavaScript object getters and setters are for on GreatFrontEnd
Tools and techniques for debugging JavaScript code vary depending on the context:
debugger statement: Inserting debugger; in code triggers breakpoints when Devtools are open, pausing execution for inspection.console.log() debugging: Using console.log() statements to output variable values and debug messages.Explore what tools and techniques are used for debugging JavaScript code on GreatFrontEnd
Currying in JavaScript is a functional programming technique where a function with multiple arguments is transformed into a sequence of nested functions, each taking a single argument. This allows for partial application of the function's arguments, meaning you can fix some arguments ahead of time and then apply the remaining arguments later.
Here's a simple example of a curry function and why this syntax offers an advantage:
// Example of a curry functionfunction curry(fn) {return function curried(...args) {if (args.length >= fn.length) {return fn(...args);} else {return function (...moreArgs) {return curried(...args, ...moreArgs);};}};}// Example function to be curriedfunction multiply(a, b, c) {return a * b * c;}// Currying the multiply functionconst curriedMultiply = curry(multiply);// Applying curried functionsconst step1 = curriedMultiply(2); // partially apply 2const step2 = step1(3); // partially apply 3const result = step2(4); // apply the final argumentconsole.log(result); // Output: 24
Advantages of Curry Syntax:
Currying enhances the functional programming paradigm in JavaScript by enabling concise, composable, and reusable functions, promoting cleaner and more modular code.
Explore examples of a curry function and why this syntax offers an advantage on GreatFrontEnd
load event and the document DOMContentLoaded event?The DOMContentLoaded event is triggered once the initial HTML document has been fully loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.
In contrast, the window's load event is fired only after the DOM and all dependent resources, such as stylesheets, images, and subframes, have completely loaded.
If three components in the same render tree call useUser(123), you don't want three identical network requests. A reasonable design has three parts:
An in-flight cache keyed by the request URL (or a stable tuple of arguments). The first caller starts the request and stores the promise; concurrent callers receive the same promise.
const inFlight = new Map();function dedupedFetch(key, requestFn) {if (inFlight.has(key)) return inFlight.get(key);const promise = requestFn().finally(() => inFlight.delete(key));inFlight.set(key, promise);return promise;}
A short-lived response cache so quick re-renders or navigations don't re-fetch. TTL depends on data freshness requirements — longer for reference data, shorter for user state.
A revalidation strategy so stale data eventually refreshes. The common pattern is "stale-while-revalidate" — serve the cached value immediately, then fetch in the background and update the cache when the new data arrives.
This is what SWR, TanStack Query, and Apollo Client provide out of the box. Rolling your own makes sense for small apps with simple needs and tight bundle budgets; adopting a library is usually the right call once you need cache invalidation, retries, or devtools support.
A useful follow-up: what if two callers pass slightly different arguments that should produce the same response? The fix is to normalize the cache key (sort query params, canonicalize the URL) before lookup.
The same-origin policy restricts how scripts loaded from one origin can interact with resources from another origin. An origin is the combination of URI scheme, hostname, and port — https://example.com and https://api.example.com are different origins.
A key nuance: the SOP doesn't usually block a cross-origin request from being sent; it blocks JavaScript from reading the response (and from accessing the DOM of cross-origin frames). That's why a cross-origin fetch will still hit the server, but the response body is hidden from your script unless the server opts in via CORS headers. This prevents malicious scripts from silently exfiltrating data the user is authorized to see on another site.
Explore how JSONP works (and how it's not really Ajax) on GreatFrontEnd
Single Page Apps (SPAs) are highly interactive web applications that load a single HTML page and dynamically update content as the user interacts with the app. Unlike traditional server-side rendering, SPAs use client-side rendering, fetching new data via AJAX without full-page refreshes. This approach makes the app more responsive and reduces the number of HTTP requests.
Pros:
Cons:
On SEO: Googlebot has run an evergreen Chromium-based renderer for years and indexes JS-rendered content, but rendering can be deferred (sometimes by days), some non-Google crawlers and AI bots don't render JS at all, and social media link previews rely on initial HTML. The standard approaches:
Explore what a single page app is and how to make one SEO-friendly on GreatFrontEnd
In the past, developers often used Backbone for models, promoting an OOP approach by creating Backbone models and attaching methods to them.
While the module pattern remains useful, modern development often favors React/Redux, which employs a single-directional data flow based on the Flux architecture. Here, app data models are typically represented using plain objects, with utility pure functions to manipulate these objects. State changes are handled using actions and reducers, following Redux principles.
Avoid classical inheritance when possible. If you must use it, adhere to best practices and guidelines.
Explore how to organize your code on GreatFrontEnd
The classic example is a search-as-you-type input that fires fetch(query) on every keystroke. A slower earlier request can resolve after a faster later one, overwriting the correct results with stale data. Three approaches, in increasing order of how well they handle it:
Token check — capture an id at the start of each call and only commit the result if the id is still the latest:
let latestRequestId = 0;async function search(query) {const myRequestId = ++latestRequestId;const results = await fetch(`/search?q=${query}`).then((r) => r.json());if (myRequestId === latestRequestId) {setResults(results);}}
AbortController — cancel the previous request before starting a new one. The fetch rejects with an AbortError, which you can ignore:
let controller;async function search(query) {controller?.abort();controller = new AbortController();try {const results = await fetch(`/search?q=${query}`, {signal: controller.signal,}).then((r) => r.json());setResults(results);} catch (e) {if (e.name === 'AbortError') return;throw e;}}
A query layer (TanStack Query, SWR) — handles deduplication, cancellation, stale-while-revalidate, and result ordering for you. Worth adopting once you have more than a handful of async data needs.
Token check is the simplest fallback and works in any environment, but the in-flight request still completes and consumes server resources. AbortController closes the connection on the client, which lets the browser stop downloading the response — though whether the server stops processing depends on whether the handler is wired to listen for the disconnect.
In React, the natural place to call controller.abort() is the cleanup function returned from useEffect, so a request started by an effect is cancelled when the component unmounts or the effect re-runs.
Attributes: Defined in HTML tags, they provide initial info for the browser (like "Hello" in <input type="text" value="Hello">).
Properties: Belong to the DOM (JavaScript's view of the page), allowing you to access and change element info after the page loads (like updating the text field value).
Explore the difference between an "attribute" and a "property" on GreatFrontEnd
At the senior level, the questions don't get harder so much as the answers get deeper. You're expected to know the language, but what differentiates a strong candidate is connecting the language to its consequences — knowing where the sharp edges are, how the framework you use leans on them, and which tradeoffs you'd defend in a code review.
Prefer GitHub? You can explore all +190 JavaScript interview questions directly in our open source repo.