JavaScript Coding Interview Questions and Solutions
Master JavaScript coding interviews with 15 frequently asked problems covering strings, arrays, recursion, debounce, throttle, Promise.all, event emitters, memoization, polyfills, deep cloning, LRU Cache, and senior-level best practices.
JavaScript Coding Interview Questions and Solutions
Introduction
JavaScript coding interviews evaluate more than your ability to produce working code. Interviewers also assess how well you understand:
- JavaScript fundamentals
- Arrays and objects
- Closures
- Higher-order functions
- Asynchronous programming
- Time and space complexity
- Immutability
- Edge-case handling
- Code readability
Frontend developers are frequently asked to implement common utility functions, JavaScript polyfills, asynchronous helpers, caching mechanisms, and reusable programming patterns.
A strong interview answer should include:
- A clear understanding of the problem.
- A simple solution before optimization.
- Time and space complexity.
- Edge-case handling.
- A production-ready implementation.
- Tests or example inputs and outputs.
This guide covers 15 frequently asked JavaScript coding interview questions with detailed solutions, explanations, complexity analysis, common mistakes, and senior-level follow-up discussions.
Q1. Reverse a String
Problem
Write a function that reverses a given string.
Example
Input: "javascript"
Output: "tpircsavaj"
Solution 1: Using Built-in Methods
function reverseString(value) {
return value.split("").reverse().join("");
}
console.log(reverseString("javascript"));
Output
tpircsavaj
Solution 2: Without Using reverse()
function reverseString(value) {
let result = "";
for (let index = value.length - 1; index >= 0; index--) {
result += value[index];
}
return result;
}
console.log(reverseString("frontend"));
Output
dnetnorf
Complexity
| Approach | Time | Space |
|---|---|---|
| Built-in methods | O(n) | O(n) |
| Reverse loop | O(n) | O(n) |
Common Mistake
Strings are immutable in JavaScript. You cannot reverse a string in place.
Senior Follow-up
JavaScript strings use UTF-16 code units. A basic split("") solution may incorrectly handle some Unicode characters.
A safer approach is:
function reverseUnicodeString(value) {
return Array.from(value).reverse().join("");
}
Q2. Check Whether a String Is a Palindrome
Problem
A palindrome reads the same forward and backward.
Example
Input: "level"
Output: true
Solution
function isPalindrome(value) {
const normalized = value
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
let left = 0;
let right = normalized.length - 1;
while (left < right) {
if (normalized[left] !== normalized[right]) {
return false;
}
left++;
right--;
}
return true;
}
console.log(isPalindrome("Level"));
console.log(isPalindrome("A man, a plan, a canal: Panama"));
console.log(isPalindrome("JavaScript"));
Output
true
true
false
Complexity
- Time: O(n)
- Space: O(n), because a normalized string is created.
Common Mistake
Comparing the original value without handling spaces, punctuation, or letter casing.
Senior Follow-up
For very large strings, normalization can be performed lazily while moving two pointers, avoiding an additional normalized copy.
Q3. Remove Duplicate Values from an Array
Problem
Return an array containing only unique values.
Example
Input: [1, 2, 2, 3, 4, 4]
Output: [1, 2, 3, 4]
Solution 1: Using Set
function removeDuplicates(values) {
return [...new Set(values)];
}
console.log(removeDuplicates([1, 2, 2, 3, 4, 4]));
Solution 2: Without Set
function removeDuplicates(values) {
const seen = new Map();
const result = [];
for (const value of values) {
if (!seen.has(value)) {
seen.set(value, true);
result.push(value);
}
}
return result;
}
Removing Duplicate Objects by Property
function removeDuplicateUsers(users) {
const uniqueUsers = new Map();
for (const user of users) {
if (!uniqueUsers.has(user.id)) {
uniqueUsers.set(user.id, user);
}
}
return [...uniqueUsers.values()];
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 1, name: "Alice Updated" }
];
console.log(removeDuplicateUsers(users));
Complexity
- Time: O(n)
- Space: O(n)
Common Mistake
Using indexOf() inside filter(), which can result in O(n²) time complexity.
Q4. Deep Clone a JavaScript Object
Problem
Create an independent deep copy of a nested JavaScript object.
Basic Solution Using structuredClone()
const original = {
id: 101,
user: {
name: "Alice",
skills: ["React", "JavaScript"]
}
};
const cloned = structuredClone(original);
cloned.user.name = "Bob";
console.log(original.user.name);
console.log(cloned.user.name);
Output
Alice
Bob
Recursive Interview Implementation
function deepClone(value, visited = new WeakMap()) {
if (value === null || typeof value !== "object") {
return value;
}
if (visited.has(value)) {
return visited.get(value);
}
if (value instanceof Date) {
return new Date(value.getTime());
}
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
if (value instanceof Map) {
const clonedMap = new Map();
visited.set(value, clonedMap);
for (const [key, item] of value) {
clonedMap.set(
deepClone(key, visited),
deepClone(item, visited)
);
}
return clonedMap;
}
if (value instanceof Set) {
const clonedSet = new Set();
visited.set(value, clonedSet);
for (const item of value) {
clonedSet.add(deepClone(item, visited));
}
return clonedSet;
}
const clone = Array.isArray(value)
? []
: Object.create(Object.getPrototypeOf(value));
visited.set(value, clone);
for (const key of Reflect.ownKeys(value)) {
clone[key] = deepClone(value[key], visited);
}
return clone;
}
Why Not Use JSON Serialization?
const copy = JSON.parse(JSON.stringify(original));
This approach does not correctly preserve:
undefined- Functions
DateMapSetBigInt- Circular references
- Custom prototypes
Complexity
- Time: O(n)
- Space: O(n)
Common Mistake
Calling the spread operator a deep copy.
const copy = { ...original };
This produces only a shallow copy.
Q5. Flatten a Nested Array
Problem
Convert a deeply nested array into a single-level array.
Example
Input: [1, [2, [3, 4], 5]]
Output: [1, 2, 3, 4, 5]
Recursive Solution
function flattenArray(values) {
const result = [];
function flatten(items) {
for (const item of items) {
if (Array.isArray(item)) {
flatten(item);
} else {
result.push(item);
}
}
}
flatten(values);
return result;
}
console.log(flattenArray([1, [2, [3, 4], 5]]));
Iterative Solution
function flattenArray(values) {
const stack = [...values];
const result = [];
while (stack.length > 0) {
const value = stack.pop();
if (Array.isArray(value)) {
stack.push(...value);
} else {
result.push(value);
}
}
return result.reverse();
}
Built-in Alternative
const result = [1, [2, [3, 4]]].flat(Infinity);
Complexity
- Time: O(n)
- Space: O(n)
Common Mistake
Using recursion without considering maximum call-stack depth for extremely nested arrays.
Q6. Implement a Debounce Function
Problem
Create a function that delays execution until the user stops triggering it for a specified period.
Solution
function debounce(callback, delay) {
let timerId;
return function (...args) {
const context = this;
clearTimeout(timerId);
timerId = setTimeout(() => {
callback.apply(context, args);
}, delay);
};
}
Usage
const search = debounce((query) => {
console.log("Searching for:", query);
}, 500);
search("r");
search("re");
search("rea");
search("react");
Only the final call executes after 500 milliseconds.
Production Use Cases
- Search suggestions
- Form validation
- Window resize handling
- Autosave
- API request reduction
Complexity
Each call performs O(1) work before scheduling the callback.
Common Mistake
Not preserving the original this context or function arguments.
Senior Follow-up: Debounce with Cancel Support
function debounce(callback, delay) {
let timerId;
function debounced(...args) {
const context = this;
clearTimeout(timerId);
timerId = setTimeout(() => {
callback.apply(context, args);
}, delay);
}
debounced.cancel = function () {
clearTimeout(timerId);
timerId = undefined;
};
return debounced;
}
Q7. Implement a Throttle Function
Problem
Create a function that limits execution to at most once during a given time interval.
Solution
function throttle(callback, interval) {
let lastExecutionTime = 0;
return function (...args) {
const currentTime = Date.now();
if (currentTime - lastExecutionTime >= interval) {
lastExecutionTime = currentTime;
callback.apply(this, args);
}
};
}
Usage
const trackScroll = throttle(() => {
console.log("Scroll tracked");
}, 1000);
window.addEventListener("scroll", trackScroll);
Debounce vs Throttle
| Debounce | Throttle |
|---|---|
| Runs after activity stops | Runs at fixed intervals |
| Best for search | Best for scrolling |
| Resets timer on every call | Limits call frequency |
| Final event is important | Continuous updates matter |
Common Mistake
Using debounce and throttle interchangeably.
Senior Follow-up
A production throttle may support:
- Leading execution
- Trailing execution
- Cancellation
- Flush behavior
Q8. Implement Promise.all()
Problem
Implement a custom version of Promise.all().
Requirements
- Preserve result order.
- Resolve when all inputs resolve.
- Reject immediately when one input rejects.
- Support non-Promise values.
- Resolve an empty array immediately.
Solution
function promiseAll(iterable) {
return new Promise((resolve, reject) => {
const values = Array.from(iterable);
const results = new Array(values.length);
if (values.length === 0) {
resolve([]);
return;
}
let completed = 0;
values.forEach((value, index) => {
Promise.resolve(value)
.then((result) => {
results[index] = result;
completed++;
if (completed === values.length) {
resolve(results);
}
})
.catch(reject);
});
});
}
Usage
promiseAll([
Promise.resolve("User"),
42,
new Promise((resolve) => {
setTimeout(() => resolve("Orders"), 100);
})
]).then(console.log);
Output
["User", 42, "Orders"]
Complexity
- Time: O(n), excluding individual asynchronous work.
- Space: O(n)
Common Mistake
Pushing results as Promises finish, which loses the original input order.
Q9. Build an Event Emitter
Problem
Create an Event Emitter supporting:
on()off()emit()once()
Solution
class EventEmitter {
constructor() {
this.events = new Map();
}
on(eventName, listener) {
if (typeof listener !== "function") {
throw new TypeError("Listener must be a function");
}
const listeners = this.events.get(eventName) ?? new Set();
listeners.add(listener);
this.events.set(eventName, listeners);
return () => this.off(eventName, listener);
}
off(eventName, listener) {
const listeners = this.events.get(eventName);
if (!listeners) {
return;
}
listeners.delete(listener);
if (listeners.size === 0) {
this.events.delete(eventName);
}
}
once(eventName, listener) {
const unsubscribe = this.on(eventName, (...args) => {
unsubscribe();
listener(...args);
});
return unsubscribe;
}
emit(eventName, ...args) {
const listeners = this.events.get(eventName);
if (!listeners) {
return false;
}
for (const listener of [...listeners]) {
listener(...args);
}
return true;
}
}
Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.on("login", (user) => {
console.log(`${user} logged in`);
});
emitter.emit("login", "Alice");
unsubscribe();
emitter.emit("login", "Bob");
Production Use Cases
- Custom UI events
- Pub/Sub systems
- Notifications
- Plugin architecture
- Analytics events
Complexity
on(): O(1)off(): O(1)emit(): O(k), wherekis the number of listeners
Common Mistake
Not removing listeners, which can cause memory leaks.
Q10. Implement Memoization
Problem
Cache function results so repeated calls with the same arguments avoid repeated computation.
Basic Solution
function memoize(callback) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = callback.apply(this, args);
cache.set(key, result);
return result;
};
}
Usage
const calculateSquare = memoize((value) => {
console.log("Calculating...");
return value * value;
});
console.log(calculateSquare(5));
console.log(calculateSquare(5));
Output
Calculating...
25
25
Limitations of JSON.stringify()
It may fail or behave unexpectedly with:
- Circular references
- Functions
- Symbols
- Different object property ordering
BigInt- Objects with equivalent content but different identities
Recursive Fibonacci Example
function createMemoizedFibonacci() {
const cache = new Map([
[0, 0],
[1, 1]
]);
function fibonacci(value) {
if (cache.has(value)) {
return cache.get(value);
}
const result = fibonacci(value - 1) + fibonacci(value - 2);
cache.set(value, result);
return result;
}
return fibonacci;
}
const fibonacci = createMemoizedFibonacci();
console.log(fibonacci(40));
Common Mistake
Using memoization without controlling cache growth.
Senior Follow-up
Production memoization may require:
- TTL expiration
- Maximum cache size
- LRU eviction
- WeakMap-based object caching
- Promise-result caching
Q11. Implement a Polyfill for Array.prototype.map()
Problem
Implement behavior similar to the native map() method.
Solution
Array.prototype.myMap = function (callback, thisArg) {
if (this == null) {
throw new TypeError("Array.prototype.myMap called on null or undefined");
}
if (typeof callback !== "function") {
throw new TypeError("Callback must be a function");
}
const source = Object(this);
const length = source.length >>> 0;
const result = new Array(length);
for (let index = 0; index < length; index++) {
if (index in source) {
result[index] = callback.call(
thisArg,
source[index],
index,
source
);
}
}
return result;
};
Usage
const numbers = [1, 2, 3];
const doubled = numbers.myMap((number) => number * 2);
console.log(doubled);
Output
[2, 4, 6]
Complexity
- Time: O(n)
- Space: O(n)
Common Mistake
Mutating the original array instead of returning a new one.
Senior Follow-up
A closer native polyfill should preserve empty slots in sparse arrays and validate the callback.
Q12. Implement a Polyfill for Array.prototype.filter()
Problem
Implement behavior similar to the native filter() method.
Solution
Array.prototype.myFilter = function (callback, thisArg) {
if (this == null) {
throw new TypeError("Array.prototype.myFilter called on null or undefined");
}
if (typeof callback !== "function") {
throw new TypeError("Callback must be a function");
}
const source = Object(this);
const length = source.length >>> 0;
const result = [];
for (let index = 0; index < length; index++) {
if (!(index in source)) {
continue;
}
const value = source[index];
if (callback.call(thisArg, value, index, source)) {
result.push(value);
}
}
return result;
};
Usage
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.myFilter(
(number) => number % 2 === 0
);
console.log(evenNumbers);
Output
[2, 4]
Complexity
- Time: O(n)
- Space: O(n)
Common Mistake
Returning transformed values instead of retaining original values that satisfy the condition.
Q13. Implement a Polyfill for Array.prototype.reduce()
Problem
Implement behavior similar to the native reduce() method.
Solution
Array.prototype.myReduce = function (callback, initialValue) {
if (this == null) {
throw new TypeError("Array.prototype.myReduce called on null or undefined");
}
if (typeof callback !== "function") {
throw new TypeError("Callback must be a function");
}
const source = Object(this);
const length = source.length >>> 0;
let index = 0;
let accumulator;
if (arguments.length >= 2) {
accumulator = initialValue;
} else {
while (index < length && !(index in source)) {
index++;
}
if (index >= length) {
throw new TypeError(
"Reduce of empty array with no initial value"
);
}
accumulator = source[index];
index++;
}
for (; index < length; index++) {
if (index in source) {
accumulator = callback(
accumulator,
source[index],
index,
source
);
}
}
return accumulator;
};
Usage
const numbers = [1, 2, 3, 4];
const total = numbers.myReduce(
(sum, number) => sum + number,
0
);
console.log(total);
Output
10
Complexity
- Time: O(n)
- Space: O(1), excluding callback-created values
Common Mistake
Not handling an empty array without an initial value.
Q14. Implement an LRU Cache
Problem
Design a Least Recently Used cache supporting:
get(key)put(key, value)
When capacity is exceeded, remove the least recently used entry.
Solution Using Map
class LRUCache {
constructor(capacity) {
if (!Number.isInteger(capacity) || capacity <= 0) {
throw new Error("Capacity must be a positive integer");
}
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) {
return -1;
}
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
const leastRecentlyUsedKey =
this.cache.keys().next().value;
this.cache.delete(leastRecentlyUsedKey);
}
}
size() {
return this.cache.size;
}
}
Usage
const cache = new LRUCache(2);
cache.put("user", "Alice");
cache.put("orders", [101, 102]);
console.log(cache.get("user"));
cache.put("products", ["Laptop"]);
console.log(cache.get("orders"));
console.log(cache.get("products"));
Output
Alice
-1
["Laptop"]
Complexity
| Operation | Time |
|---|---|
get() |
O(1) average |
put() |
O(1) average |
| Space | O(capacity) |
Production Use Cases
- API response caching
- Image caching
- Search result caching
- Database query caching
- Recently viewed items
Common Mistake
Using a plain object and array combination that requires O(n) removal.
Q15. Build a Simplified Promise from Scratch
Problem
Implement a simplified Promise supporting:
- Pending, fulfilled, and rejected states
then()catch()- Asynchronous handler execution
- Basic chaining
Solution
class SimplePromise {
constructor(executor) {
this.state = "pending";
this.value = undefined;
this.handlers = [];
const resolve = (value) => {
this.settle("fulfilled", value);
};
const reject = (reason) => {
this.settle("rejected", reason);
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
settle(state, value) {
if (this.state !== "pending") {
return;
}
this.state = state;
this.value = value;
queueMicrotask(() => {
this.runHandlers();
});
}
runHandlers() {
if (this.state === "pending") {
return;
}
const handlers = this.handlers;
this.handlers = [];
for (const handler of handlers) {
this.runHandler(handler);
}
}
runHandler(handler) {
const callback =
this.state === "fulfilled"
? handler.onFulfilled
: handler.onRejected;
if (typeof callback !== "function") {
if (this.state === "fulfilled") {
handler.resolve(this.value);
} else {
handler.reject(this.value);
}
return;
}
try {
const result = callback(this.value);
if (
result &&
typeof result.then === "function"
) {
result.then(handler.resolve, handler.reject);
} else {
handler.resolve(result);
}
} catch (error) {
handler.reject(error);
}
}
then(onFulfilled, onRejected) {
return new SimplePromise((resolve, reject) => {
const handler = {
onFulfilled,
onRejected,
resolve,
reject
};
this.handlers.push(handler);
if (this.state !== "pending") {
queueMicrotask(() => {
this.runHandlers();
});
}
});
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
}
Usage
const promise = new SimplePromise((resolve) => {
setTimeout(() => {
resolve(10);
}, 100);
});
promise
.then((value) => value * 2)
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error);
});
Output
20
Important Note
This is a simplified educational implementation. A complete standards-compliant Promise must correctly handle:
- Thenable assimilation
- Self-resolution protection
- Promise resolution procedure
- Multiple chaining edge cases
- Static methods
- Specification-level microtask behavior
Common Mistake
Executing .then() callbacks synchronously. Native Promise handlers execute asynchronously through the microtask queue.
Common JavaScript Coding Interview Mistakes
- Starting to code without clarifying requirements.
- Ignoring invalid or empty inputs.
- Using built-in methods without understanding their complexity.
- Mutating input objects or arrays unexpectedly.
- Forgetting asynchronous error handling.
- Not preserving
thisand arguments in utility functions. - Giving a solution without discussing time and space complexity.
- Writing clever code that is difficult to understand.
- Ignoring Unicode, sparse arrays, circular references, or cache growth.
- Claiming a simplified polyfill is fully standards-compliant.
Senior Developer Best Practices
- Clarify expected input, output, constraints, and edge cases before coding.
- Start with a readable solution before optimizing.
- Explain time and space complexity clearly.
- Prefer descriptive names over abbreviated variables.
- Avoid mutating caller-owned inputs unless explicitly required.
- Handle errors and invalid inputs appropriately.
- Separate reusable logic into small functions or classes.
- Explain limitations honestly.
- Add test cases covering normal, boundary, and failure scenarios.
- Discuss how the solution would change in a production environment.
Interview Quick Revision
| Problem | Primary Concept |
|---|---|
| Reverse String | String iteration |
| Palindrome | Two pointers |
| Remove Duplicates | Set / Map |
| Deep Clone | Recursion / WeakMap |
| Flatten Array | Recursion / Stack |
| Debounce | Closures / Timers |
| Throttle | Rate limiting |
| Promise.all | Async coordination |
| Event Emitter | Pub/Sub |
| Memoization | Caching / Closures |
map() Polyfill |
Array iteration |
filter() Polyfill |
Predicate filtering |
reduce() Polyfill |
Accumulation |
| LRU Cache | Map / Eviction |
| Promise Implementation | Async state machine |
JavaScript Coding Interview Workflow
graph TD
Read_Problem[Read Problem] --> Clarify_Requirements[Clarify Requirements]
Clarify_Requirements[Clarify Requirements] --> Identify_Inputs_and_Outputs[Identify Inputs and Outputs]
Identify_Inputs_and_Outputs[Identify Inputs and Outputs] --> Discuss_Edge_Cases[Discuss Edge Cases]
Discuss_Edge_Cases[Discuss Edge Cases] --> Choose_Data_Structures[Choose Data Structures]
Choose_Data_Structures[Choose Data Structures] --> Write_Readable_Solution[Write Readable Solution]
Write_Readable_Solution[Write Readable Solution] --> Test_with_Examples[Test with Examples]
Test_with_Examples[Test with Examples] --> Analyze_Complexity[Analyze Complexity]
Analyze_Complexity[Analyze Complexity] --> Discuss_Optimizations[Discuss Optimizations]
Common Complexity Summary
| Problem | Time Complexity | Space Complexity |
|---|---|---|
| Reverse String | O(n) | O(n) |
| Palindrome | O(n) | O(n) |
| Remove Duplicates | O(n) | O(n) |
| Deep Clone | O(n) | O(n) |
| Flatten Array | O(n) | O(n) |
| Debounce Call | O(1) | O(1) |
| Throttle Call | O(1) | O(1) |
| Promise.all Coordination | O(n) | O(n) |
| Event Emit | O(k) | O(k listeners) |
| Memoization Lookup | O(1) average | O(cache size) |
| Array Polyfills | O(n) | Depends on result |
| LRU Cache | O(1) average | O(capacity) |
Production JavaScript Utility Architecture
Application
│
├── String Utilities
├── Array Utilities
├── Async Utilities
├── Event Utilities
├── Cache Utilities
├── Validation Utilities
└── Test Suites
Recommended Test Cases
For every coding problem, test:
graph TD
Normal_Input[Normal Input] --> Empty_Input[Empty Input]
Empty_Input[Empty Input] --> Single_Value[Single Value]
Single_Value[Single Value] --> Duplicate_Values[Duplicate Values]
Duplicate_Values[Duplicate Values] --> Invalid_Input[Invalid Input]
Invalid_Input[Invalid Input] --> Large_Input[Large Input]
Large_Input[Large Input] --> Failure_Scenario[Failure Scenario]
Key Takeaways
- JavaScript coding interviews test language fundamentals, problem solving, complexity analysis, and production thinking.
- String and array problems evaluate iteration, two-pointer techniques, recursion, and data structures.
- Debounce and Throttle test closures, timers, and event-performance optimization.
- Custom
Promise.all()and Promise implementations test asynchronous programming and the Event Loop. - Event Emitters demonstrate the Publish–Subscribe pattern and listener management.
- Memoization and LRU Cache implementations test caching strategies and memory trade-offs.
- Polyfill questions evaluate whether you understand native JavaScript method behavior beyond basic syntax.
- Deep cloning requires careful handling of nested structures, circular references, and special object types.
- Strong candidates explain assumptions, edge cases, complexity, limitations, and production trade-offs.
- Readable, testable, and maintainable code is more valuable than unnecessarily clever solutions.