96 JavaScript Logic Building Questions — From Basic to Advanced (With Solutions)

You have finished the notes. Variables, loops, functions, objects, DOM — all of it makes sense while reading. Then an interviewer says "reverse a number without converting it to a string," and the screen stays blank.
That gap is not a syntax problem. It is a logic problem, and the only fix is solving problems in a graded order instead of randomly jumping between YouTube playlists. This is that order: 96 questions across loops, strings, arrays, functions, objects, async, design patterns, data structures, dynamic programming and real production scenarios, each with a working solution and the one detail interviewers actually listen for.
How to Practice These (4 Rules)
- Write the brute-force version first, then optimize. An interviewer wants to see that you can produce a working answer under pressure. Optimization is the follow-up conversation, not the entry ticket.
- Ban the built-in on the first attempt. Solve reverse without .reverse(), max without Math.max(), dedupe without Set. Then write the one-line version. You need both: the loop proves you understand it, the one-liner proves you can ship it.
- Say the time complexity out loud. "This is O(n²) because of the nested indexOf" scores higher than a silently correct answer.
- Test three inputs every time: a normal one, an empty one, and a nasty one (negative number, duplicate values, single character). Most rejections happen on edge cases, not on the main logic.
Level 1 — Loops and Numbers
Q1
FizzBuzz — print 1 to 100. Multiples of 3 → Fizz, of 5 → Buzz, of both → FizzBuzz.
for (let i = 1; i <= 100; i++) {
let out = "";
if (i % 3 === 0) out += "Fizz";
if (i % 5 === 0) out += "Buzz";
console.log(out || i);
}The detail: building the string instead of writing a four-branch if-else means adding a 'multiple of 7 → Bazz' rule costs one line, not a rewrite. That is what the question is really testing.
Q2
Reverse a number without converting it to a string.
function reverseNumber(n) {
const sign = Math.sign(n);
n = Math.abs(n);
let rev = 0;
while (n > 0) {
rev = rev * 10 + (n % 10);
n = Math.floor(n / 10);
}
return rev * sign;
}
// reverseNumber(-1230) → -321The detail: the string shortcut String(n).split("").reverse().join("") produces "0321-" for -1230. Handle the sign separately.
Q3
Check if a number is prime.
function isPrime(n) {
if (n < 2) return false;
if (n % 2 === 0) return n === 2;
for (let i = 3; i * i <= n; i += 2) {
if (n % i === 0) return false;
}
return true;
}The detail: i * i <= n instead of i <= Math.sqrt(n) avoids recomputing a square root every iteration and dodges floating-point edge cases. Skipping even divisors halves the loop.
Q4
Nth Fibonacci number.
function fib(n) {
let [a, b] = [0, 1];
for (let i = 0; i < n; i++) [a, b] = [b, a + b];
return a;
}The detail: the recursive version is O(2ⁿ) — fib(45) will freeze the tab. If you write recursion here, add memoization in the same breath or the interviewer will.
Q5
Armstrong number — a number equal to the sum of its digits raised to the power of the digit count (153 = 1³ + 5³ + 3³).
function isArmstrong(n) {
const digits = String(n).split("");
const power = digits.length;
const sum = digits.reduce((s, d) => s + Number(d) ** power, 0);
return sum === n;
}The detail: the digit count is recomputed as the power for each digit, so the same function works for 3-digit and higher Armstrong numbers without changes.
Q6
Print a pyramid pattern.
function pyramid(rows) {
for (let i = 1; i <= rows; i++) {
console.log(" ".repeat(rows - i) + "*".repeat(2 * i - 1));
}
}The detail: almost every pattern question in Indian campus interviews is just two formulas — spaces and characters per row. Find the formula and repeat() replaces the nested loop entirely.
Level 2 — Strings
Q7
Reverse a string without .reverse().
function reverseString(str) {
let out = "";
for (let i = str.length - 1; i >= 0; i--) out += str[i];
return out;
}The detail: str.split("") splits emoji and accented characters into broken halves. [...str] iterates by code point and keeps them intact — worth mentioning if the input might be user-generated.
Q8
Palindrome check.
function isPalindrome(str) {
const clean = str.toLowerCase().replace(/[^a-z0-9]/g, "");
let i = 0, j = clean.length - 1;
while (i < j) {
if (clean[i] !== clean[j]) return false;
i++; j--;
}
return true;
}
// "A man, a plan, a canal: Panama" → trueThe detail: the two-pointer version uses no extra memory and exits early on the first mismatch. Normalizing case and punctuation before comparing is half the marks.
Q9
Count vowels in a string.
const countVowels = str => (str.match(/[aeiou]/gi) || []).length;
// "xyz".match(/[aeiou]/gi) → null, not []The detail: match() returns null — not an empty array — when nothing is found. Calling .length directly on that null throws. The || [] is the whole question.
Q10
First non-repeating character.
function firstUnique(str) {
const count = new Map();
for (const ch of str) count.set(ch, (count.get(ch) || 0) + 1);
for (const ch of str) if (count.get(ch) === 1) return ch;
return null;
}
// firstUnique("swiss") → "w"The detail: two passes at O(n) beats the indexOf === lastIndexOf trick, which is O(n²). Map also preserves insertion order, so the first match really is the first.
Q11
Anagram check.
const isAnagram = (a, b) => {
const norm = s => [...s.toLowerCase().replace(/\s/g, "")].sort().join("");
return norm(a) === norm(b);
};The detail: sorting is O(n log n). The optimized answer builds one frequency map, then decrements it with the second string — O(n). Say that even if you submit the sort version.
Q12
Convert a sentence to title case.
const toTitleCase = str =>
str
.split(" ")
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");The detail: charAt(0) on an empty string (from double spaces) silently returns an empty string rather than throwing, which is why this version survives messy input better than str[0].
Q13
Longest substring without repeating characters (advanced).
function longestUnique(str) {
const seen = new Map();
let start = 0, best = 0;
for (let i = 0; i < str.length; i++) {
const ch = str[i];
if (seen.has(ch) && seen.get(ch) >= start) {
start = seen.get(ch) + 1;
}
seen.set(ch, i);
best = Math.max(best, i - start + 1);
}
return best;
}
// longestUnique("abcabcbb") → 3The detail: the seen.get(ch) >= start check. Without it, a repeat that already fell outside the window drags start backwards and quietly returns a wrong answer on inputs like 'abba'. This is the sliding-window pattern — once you have it, a dozen other problems become the same problem.
Level 3 — Arrays
Q14
Second largest element.
function secondLargest(arr) {
let first = -Infinity, second = -Infinity;
for (const n of arr) {
if (n > first) { second = first; first = n; }
else if (n > second && n !== first) second = n;
}
return second === -Infinity ? null : second;
}The detail: arr.sort((a,b) => b-a)[1] returns 5 for [5, 5, 3], which is wrong. And a bare arr.sort() sorts as text, so [10, 9] stays [10, 9]. Duplicates are the whole trap here.
Q15
Remove duplicates from an array.
const unique = arr => [...new Set(arr)];The detail: Set compares by reference, so an array of objects will not dedupe — {id:1} and {id:1} are two different values. For objects, dedupe through a Map keyed on a property.
Q16
Move all zeros to the end, in place.
function moveZeros(arr) {
let insert = 0;
for (let i = 0; i < arr.length; i++) {
if (arr[i] !== 0) {
[arr[insert], arr[i]] = [arr[i], arr[insert]];
insert++;
}
}
return arr;
}
// [0,1,0,3,12] → [1,3,12,0,0]The detail: "in place" means no new array. The two-pointer swap keeps the relative order of non-zero values, which the naive filter + concat version also does but at the cost of extra memory.
Q17
Two Sum.
function twoSum(arr, target) {
const seen = new Map();
for (let i = 0; i < arr.length; i++) {
const need = target - arr[i];
if (seen.has(need)) return [seen.get(need), i];
seen.set(arr[i], i);
}
return [];
}The detail: check for the complement before storing the current number, otherwise [3,3] with target 6 matches an element with itself.
Q18
Rotate an array by k positions.
function rotate(arr, k) {
k = k % arr.length;
return [...arr.slice(-k), ...arr.slice(0, -k)];
}The detail: the modulo. Without it, k = 7 on a 5-element array returns an empty result instead of wrapping around.
Q19
Flatten a deeply nested array.
function flatten(arr) {
return arr.reduce(
(acc, val) => acc.concat(Array.isArray(val) ? flatten(val) : val),
[]
);
}The detail: arr.flat(Infinity) does this in one line — write the recursion only when explicitly asked. Also note that recursion blows the call stack on pathologically deep input; an explicit stack-based loop does not.
Q20
Chunk an array into groups of n.
function chunk(arr, size) {
const out = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
// chunk([1,2,3,4,5], 2) → [[1,2],[3,4],[5]]The detail: slice() never throws when the end index overruns the array, so the last chunk is simply shorter — no extra bounds-checking needed.
Q21
Find the missing number from 1…n.
const missing = (arr, n) =>
(n * (n + 1)) / 2 - arr.reduce((a, b) => a + b, 0);The detail: O(n) time, O(1) space, no sorting. The XOR variant avoids integer overflow when n is very large — a good line to add when the interviewer asks 'can you do better.'
Level 4 — Functions and Closures
Q22
Counter using closure.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
reset: () => (count = 0),
get value() { return count; }
};
}The detail: count is unreachable from outside — this is how private state worked in JavaScript long before #privateFields. Expect the follow-up: 'why does a var loop with setTimeout print 3, 3, 3?'
Q23
Write your own map() polyfill.
Array.prototype.myMap = function (callback, thisArg) {
const out = [];
for (let i = 0; i < this.length; i++) {
if (i in this) out[i] = callback.call(thisArg, this[i], i, this);
}
return out;
};The detail: if (i in this). The real map skips holes in sparse arrays — [1, , 3].map(x => x * 2) keeps the hole. That single line separates someone who memorized the answer from someone who read the spec.
Q24
Currying — make sum(1)(2)(3) work.
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length
? fn.apply(this, args)
: (...next) => curried.apply(this, [...args, ...next]);
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6The detail: this relies on fn.length, the declared arity. Default parameters and rest parameters make fn.length smaller or zero, so currying silently breaks on (a, b = 2) => ... . Know that before you use it in real code.
Q25
Debounce.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}The detail: every call resets the timer, so fn only fires once activity has fully stopped for delay milliseconds — right for a search box hitting an API.
Q26
Throttle.
function throttle(fn, limit) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= limit) {
last = now;
fn.apply(this, args);
}
};
}The detail: debounce waits until the activity stops — right for a search box. Throttle fires at most once per interval regardless — right for scroll and resize handlers. Interviewers ask which one you would use where far more often than they ask you to implement either.
Q27
Memoize any function.
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}The detail: the JSON.stringify key is fine for primitive arguments and unsafe for objects — key order changes the string, and functions or undefined vanish during serialization. Say this proactively; it shows you have used memoization outside a tutorial.
Level 5 — Objects
Q28
Word frequency counter.
function wordCount(text) {
return (text.toLowerCase().match(/\b\w+\b/g) || []).reduce((acc, w) => {
acc[w] = (acc[w] || 0) + 1;
return acc;
}, {});
}The detail: a plain {} inherits from Object.prototype, so counting the words "constructor" or "toString" gives bizarre results. Use Object.create(null) or a Map for untrusted text.
Q29
Deep clone an object.
const copy = structuredClone(original);The detail: the classic JSON.parse(JSON.stringify(obj)) silently drops undefined, functions and Symbols, converts Date objects into strings, and throws on circular references. structuredClone handles Dates, Maps, Sets and circular refs — but not functions. Know which limitation you are accepting.
Q30
Group an array of objects by a key.
function groupBy(arr, key) {
return arr.reduce((acc, item) => {
const k = typeof key === "function" ? key(item) : item[key];
(acc[k] ||= []).push(item);
return acc;
}, {});
}
// groupBy(students, "city")The detail: Object.groupBy() is now built into modern browsers and Node 21+. Writing the reduce version still matters, because reduce accumulating into an object is the single most common real-world pattern in this entire list.
Q31
Sort an array of objects.
const byAge = (a, b) => a.age - b.age;
const byName = (a, b) => a.name.localeCompare(b.name);
const sorted = [...users].sort(byName);The detail: sort() mutates the original array. Spread it first, or use toSorted(). And localeCompare — not > — is what handles case and accented characters correctly.
Q32
Deep equality check.
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
const ka = Object.keys(a), kb = Object.keys(b);
if (ka.length !== kb.length) return false;
return ka.every(k => deepEqual(a[k], b[k]));
}The detail: Object.is instead of === in the first line, so deepEqual(NaN, NaN) returns true. Arrays and Dates need explicit handling if the input can contain them.
Q33
Flatten a nested object into dot notation.
function flattenObject(obj, prefix = "") {
return Object.keys(obj).reduce((acc, k) => {
const path = prefix ? `${prefix}.${k}` : k;
if (obj[k] && typeof obj[k] === "object" && !Array.isArray(obj[k])) {
Object.assign(acc, flattenObject(obj[k], path));
} else {
acc[path] = obj[k];
}
return acc;
}, {});
}
// { user: { address: { city: "Pune" } } } → { "user.address.city": "Pune" }Why this one matters: it is the exact logic behind form libraries, i18n files and config loaders. Ask any backend developer how often they have written it.
Level 6 — Output-Based and Async
These are prediction questions. No code to write — you have to say what prints, and why.
Q34
var vs let inside a loop — what prints?
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
// 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i));
// 0 1 2Why: var creates one binding shared by all three callbacks, and by the time they run the loop has finished. let creates a fresh binding per iteration.
Q35
Event loop ordering — what prints, and in what order?
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
queueMicrotask(() => console.log("4"));
console.log("5");
// 1 5 3 4 2Why: all synchronous code runs first, then the entire microtask queue (promises, queueMicrotask), and only then the macrotask queue (setTimeout). A setTimeout(fn, 0) is never actually zero.
Q36
this in a regular function vs an arrow function.
const user = {
name: "Suhas",
regular() { return this.name; }, // "Suhas"
arrow: () => this?.name // undefined
};Why: an arrow function has no this of its own — it takes it from the scope where it was defined. Rule of thumb: never an arrow for an object method, always an arrow for a callback inside a method.
Q37
Implement Promise.all.
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
if (promises.length === 0) return resolve([]);
promises.forEach((p, i) => {
Promise.resolve(p).then(value => {
results[i] = value;
if (++completed === promises.length) resolve(results);
}, reject);
});
});
}The detail: results[i] = value, never results.push(value). Push returns results in completion order; the real Promise.all guarantees input order. Also: an empty array must resolve immediately, and one rejection rejects the whole thing.
Q38
Retry an async call with exponential backoff.
async function retry(fn, attempts = 3, delay = 500) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, delay * 2 ** i));
}
}
}Why this closes the list: it combines loops, closures, promises and error handling in eight lines, and it is real production code — every payment gateway and third-party API integration needs it. If you can write this without help, you have finished the logic-building stage.
Level 7 — Design Patterns and Utilities
Q39
Implement a Pub/Sub event emitter class.
class EventEmitter {
constructor() { this.events = new Map(); }
on(event, cb) {
if (!this.events.has(event)) this.events.set(event, []);
this.events.get(event).push(cb);
return () => this.off(event, cb);
}
off(event, cb) {
const cbs = this.events.get(event);
if (cbs) this.events.set(event, cbs.filter(fn => fn !== cb));
}
emit(event, ...args) {
(this.events.get(event) || []).forEach(cb => cb(...args));
}
}The detail: on() returning an unsubscribe function is the detail interviewers want — it means callers never need to hold a reference to the original callback just to remove it later.
Q40
Implement your own Function.prototype.bind() polyfill.
Function.prototype.myBind = function (ctx, ...boundArgs) {
const fn = this;
return function (...args) {
return fn.apply(ctx, [...boundArgs, ...args]);
};
};The detail: bind must support partial application — arguments passed at bind time and at call time both need to reach the original function, in that order.
Q41
Implement compose() and pipe().
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);The detail: compose applies right-to-left (mathematical composition), pipe applies left-to-right (reads like a sentence) — mixing them up is the most common mistake in an otherwise correct answer.
Q42
Implement a simple LRU cache using a Map.
class LRUCache {
constructor(capacity) { this.capacity = capacity; this.cache = new Map(); }
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
else if (this.cache.size >= this.capacity) this.cache.delete(this.cache.keys().next().value);
this.cache.set(key, value);
}
}The detail: a JS Map preserves insertion order, so deleting and re-setting a key on every access is enough to track recency — the first key in iteration order is always the least recently used one.
Q43
Deep merge two objects.
function deepMerge(a, b) {
const out = { ...a };
for (const key of Object.keys(b)) {
if (b[key] && typeof b[key] === "object" && !Array.isArray(b[key]) && a[key]) {
out[key] = deepMerge(a[key], b[key]);
} else {
out[key] = b[key];
}
}
return out;
}The detail: arrays are intentionally not merged element-by-element — b's array simply replaces a's, because merging arrays has no single correct semantics (concat, overwrite, dedupe) and guessing wrong is worse than being explicit.
Q44
Implement the Singleton pattern using closures.
const Singleton = (() => {
let instance;
function create() { return { id: Math.random() }; }
return { getInstance: () => instance ?? (instance = create()) };
})();The detail: the instance lives in the closure's scope, not on the object itself, so nothing outside getInstance() can ever construct a second one — no static class property or module-level mutable export needed.
Q45
Implement a minimal Promise (then/catch) from scratch.
class MyPromise {
constructor(executor) {
this.state = "pending"; this.value = undefined; this.callbacks = [];
const resolve = value => {
if (this.state !== "pending") return;
this.state = "fulfilled"; this.value = value;
this.callbacks.forEach(cb => cb.onFulfilled(value));
};
const reject = reason => {
if (this.state !== "pending") return;
this.state = "rejected"; this.value = reason;
this.callbacks.forEach(cb => cb.onRejected(reason));
};
try { executor(resolve, reject); } catch (e) { reject(e); }
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handle = () => {
try {
if (this.state === "fulfilled") resolve(onFulfilled ? onFulfilled(this.value) : this.value);
else if (this.state === "rejected") {
if (onRejected) resolve(onRejected(this.value)); else reject(this.value);
}
} catch (e) { reject(e); }
};
if (this.state === "pending") this.callbacks.push({ onFulfilled: handle, onRejected: handle });
else handle();
});
}
}The detail: state can only transition once — resolve/reject after the first call are silently ignored — and callbacks registered after settlement must still fire, which is why pending callbacks queue while settled ones run immediately.
Q46
Implement a sleep/delay utility with async/await.
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function run() {
console.log("start");
await sleep(1000);
console.log("1 second later");
}The detail: sleep() itself does not block anything — it just returns a Promise that resolves later; await is what makes the calling async function pause without blocking the thread pool or the event loop.
Q47
Implement a basic Observable (single-subscriber stream).
class Observable {
constructor(subscribeFn) { this._subscribe = subscribeFn; }
subscribe(observer) { return this._subscribe(observer); }
static of(...values) {
return new Observable(observer => {
values.forEach(v => observer.next(v));
observer.complete();
});
}
}The detail: nothing runs until subscribe() is called — this is what 'lazy' means for an Observable, unlike a Promise, which starts executing the moment it is constructed.
Q48
Deep freeze an object recursively.
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach(key => {
const val = obj[key];
if (val && typeof val === "object" && !Object.isFrozen(val)) deepFreeze(val);
});
return Object.freeze(obj);
}The detail: Object.freeze() is shallow — it stops reassignment of top-level keys but nested objects stay mutable. Interviewers use this to check whether you know the difference, not whether you can write the loop.
Level 8 — Data Structures and Algorithms
Q49
Implement a Stack using a class.
class Stack {
#items = [];
push(item) { this.#items.push(item); }
pop() { return this.#items.pop(); }
peek() { return this.#items[this.#items.length - 1]; }
get isEmpty() { return this.#items.length === 0; }
}The detail: using a private class field (#items) instead of a plain property stops consumers from mutating the internal array directly and bypassing push/pop — the same private-state idea as the closure counter from Level 4.
Q50
Implement a Queue using a class.
class Queue {
#items = [];
enqueue(item) { this.#items.push(item); }
dequeue() { return this.#items.shift(); }
get front() { return this.#items[0]; }
}The detail: shift() is O(n) because every remaining element shifts down one index — fine for interview code, but a production high-throughput queue should use two stacks or a circular buffer instead.
Q51
Binary search on a sorted array.
function binarySearch(arr, target) {
let lo = 0, hi = arr.length - 1;
while (lo <= hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) lo = mid + 1; else hi = mid - 1;
}
return -1;
}The detail: lo + Math.floor((hi - lo) / 2) instead of Math.floor((lo + hi) / 2) avoids integer overflow on very large arrays in languages with fixed-width integers — not a real risk in JS numbers, but interviewers still expect you to know why the safer form exists.
Q52
Implement a singly linked list with insert and traverse.
class Node { constructor(value) { this.value = value; this.next = null; } }
class LinkedList {
constructor() { this.head = null; }
insert(value) {
const node = new Node(value);
if (!this.head) { this.head = node; return; }
let cur = this.head;
while (cur.next) cur = cur.next;
cur.next = node;
}
toArray() {
const out = []; let cur = this.head;
while (cur) { out.push(cur.value); cur = cur.next; }
return out;
}
}The detail: insert() here is O(n) because it walks to the tail every time — mention that you would keep a tail pointer to make it O(1) if the interviewer asks you to optimize.
Q53
Find the longest common prefix among an array of strings.
function longestCommonPrefix(strs) {
if (strs.length === 0) return "";
let prefix = strs[0];
for (let i = 1; i < strs.length; i++) {
while (!strs[i].startsWith(prefix)) prefix = prefix.slice(0, -1);
if (prefix === "") return "";
}
return prefix;
}The detail: shrinking the prefix from the current candidate is simpler than comparing character-by-character across all strings at once, and it naturally handles the empty-array and no-common-prefix edge cases.
Q54
Check for balanced parentheses and brackets.
function isBalanced(str) {
const pairs = { ")": "(", "]": "[", "}": "{" };
const stack = [];
for (const ch of str) {
if (ch === "(" || ch === "[" || ch === "{") stack.push(ch);
else if (ch in pairs) {
if (stack.pop() !== pairs[ch]) return false;
}
}
return stack.length === 0;
}The detail: the final stack.length === 0 check is the one people forget — "([)" fails correctly without it by accident, but an unclosed "((" would incorrectly return true.
Q55
Implement quicksort.
function quicksort(arr) {
if (arr.length <= 1) return arr;
const [pivot, ...rest] = arr;
const left = rest.filter(n => n < pivot);
const right = rest.filter(n => n >= pivot);
return [...quicksort(left), pivot, ...quicksort(right)];
}The detail: this version is O(n) extra space per level because filter() allocates new arrays — say so, and mention that an in-place partition (Lomuto or Hoare) is what production sort implementations actually use.
Q56
Find the intersection of two arrays.
function intersection(a, b) {
const setB = new Set(b);
return [...new Set(a)].filter(x => setB.has(x));
}The detail: wrapping a in a Set before filtering removes duplicates from the result and turns the membership check into O(1) instead of O(n), taking the whole function from O(n*m) to O(n+m).
Q57
Generate all permutations of an array.
function permute(arr) {
if (arr.length <= 1) return [arr];
const result = [];
arr.forEach((item, i) => {
const rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
permute(rest).forEach(p => result.push([item, ...p]));
});
return result;
}The detail: the result set grows factorially (n! permutations), so this is only reasonable for small n — say that out loud so the interviewer knows you understand the complexity, not just the recursion.
Q58
Rotate a matrix 90 degrees in place.
function rotateMatrix(m) {
const n = m.length;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
[m[i][j], m[j][i]] = [m[j][i], m[i][j]];
}
}
m.forEach(row => row.reverse());
return m;
}The detail: transpose-then-reverse-each-row is the standard trick — transposing alone would give a 90° rotation mirrored the wrong way, which is why the row reversal step is not optional.
Level 9 — Generators, Async Patterns and Metaprogramming
Q59
Implement a custom iterable using Symbol.iterator (a range generator).
function range(start, end, step = 1) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current < end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
};
}
// [...range(0, 10, 2)] → [0, 2, 4, 6, 8]The detail: implementing Symbol.iterator yourself is what makes an object work with for...of and spread — a generator function (function*) does this same job with far less boilerplate, which is worth mentioning as the 'real' answer.
Q60
Implement an infinite lazy sequence with a generator and a take(n) helper.
function* naturals() {
let n = 1;
while (true) yield n++;
}
function take(iterable, n) {
const out = [];
for (const val of iterable) {
if (out.length >= n) break;
out.push(val);
}
return out;
}
// take(naturals(), 5) → [1, 2, 3, 4, 5]The detail: naturals() never runs its while(true) loop to completion — each yield pauses execution until the consumer asks for the next value, so an infinite generator is safe as long as the consumer knows when to stop.
Q61
Implement an async generator that pages through an API.
async function* paginate(fetchPage) {
let page = 1;
while (true) {
const { items, hasMore } = await fetchPage(page++);
yield* items;
if (!hasMore) return;
}
}
// for await (const item of paginate(fetchPage)) { ... }The detail: for await...of is required here, not for...of — the generator yields items but the fetch itself is async, so the consumer must await each step of iteration too.
Q62
Implement Function.prototype.call and .apply polyfills.
Function.prototype.myCall = function (ctx, ...args) {
ctx = ctx || globalThis;
const key = Symbol("fn");
ctx[key] = this;
const result = ctx[key](...args);
delete ctx[key];
return result;
};
Function.prototype.myApply = function (ctx, argsArray = []) {
return this.myCall(ctx, ...argsArray);
};The detail: temporarily attaching the function to ctx as a property is what makes this inside the call resolve to ctx — call/apply do not run the function standalone, they run it as a method of the given object.
Q63
Implement a Proxy-based reactive object with a change callback (mini Vue-style reactivity).
function reactive(target, onChange) {
return new Proxy(target, {
set(obj, key, value) {
obj[key] = value;
onChange(key, value);
return true;
}
});
}
const state = reactive({ count: 0 }, (key, value) => console.log(`${key} changed to ${value}`));
state.count++; // logs "count changed to 1"The detail: the set trap intercepts every assignment, including ones from ++ and destructuring — this single hook is the entire mechanism behind Vue 3's reactivity system, just without the dependency graph.
Q64
Use a WeakMap to give a class private state (an alternative to closures).
const privateData = new WeakMap();
class BankAccount {
constructor(balance) { privateData.set(this, { balance }); }
deposit(amount) { privateData.get(this).balance += amount; }
get balance() { return privateData.get(this).balance; }
}The detail: keying the WeakMap on the instance means the private data is garbage-collected automatically when the instance is, unlike a plain Map, which would keep every instance alive forever as a key.
Q65
Implement an async task pool that runs at most N promises concurrently.
async function asyncPool(tasks, limit) {
const results = [];
const executing = new Set();
for (const [i, task] of tasks.entries()) {
const p = Promise.resolve().then(() => task()).then(r => { results[i] = r; });
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) await Promise.race(executing);
}
await Promise.all(executing);
return results;
}The detail: Promise.race(executing) is what enforces the limit — it pauses the loop until any one running task finishes and frees a slot, instead of waiting for all of them the way Promise.all would.
Q66
Implement a curry function that supports a placeholder argument.
const _ = Symbol("placeholder");
function curry(fn) {
return function curried(...args) {
const complete = args.length >= fn.length && !args.includes(_);
if (complete) return fn(...args);
return (...next) => {
const merged = args.map(a => (a === _ ? next.shift() : a)).concat(next);
return curried(...merged);
};
};
}
const add3 = curry((a, b, c) => a + b + c);
add3(1, _, 3)(2); // 6The detail: the placeholder lets a caller fix some arguments while deferring others out of order — the merge step walks the existing args array replacing placeholders before appending anything left over in next.
Q67
Implement a tagged template literal that escapes interpolated values for safe HTML.
function safeHtml(strings, ...values) {
const escape = str => String(str)
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
return strings.reduce((out, str, i) => out + str + (i < values.length ? escape(values[i]) : ""), "");
}
const name = "<script>alert(1)</script>";
safeHtml`Hello, ${name}!`; // "Hello, <script>alert(1)</script>!"The detail: only the interpolated values get escaped, never the literal template text — that is what lets the template author write real HTML while still neutralizing anything coming from user input.
Q68
Write your own Array.prototype.flat polyfill.
Array.prototype.myFlat = function (depth = 1) {
return depth > 0
? this.reduce((acc, val) => acc.concat(Array.isArray(val) ? val.myFlat(depth - 1) : val), [])
: this.slice();
};The detail: depth decrements on each recursive call, so myFlat(Infinity) keeps flattening until nothing is left — passing depth 0 correctly returns a shallow copy instead of an empty array.
Level 10 — Dynamic Programming and Tricky Output
Q69
Fibonacci with memoization (top-down DP).
function fibMemo(n, cache = new Map()) {
if (n <= 1) return n;
if (cache.has(n)) return cache.get(n);
const result = fibMemo(n - 1, cache) + fibMemo(n - 2, cache);
cache.set(n, result);
return result;
}
// fibMemo(45) resolves instantly; the plain recursive version from Level 1 does not.The detail: this is the direct fix to the O(2ⁿ) recursive Fibonacci from Level 1 — the cache turns overlapping subproblems into O(n) work by never recomputing the same n twice.
Q70
Longest Common Subsequence between two strings.
function lcs(a, b) {
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
dp[i][j] = a[i - 1] === b[j - 1]
? dp[i - 1][j - 1] + 1
: Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[a.length][b.length];
}
// lcs("abcde", "ace") → 3The detail: the table's [i][j] cell answers 'LCS of the first i characters of a and first j of b' — building it bottom-up avoids the exponential blowup of trying every subsequence directly.
Q71
Coin change — minimum coins needed to make an amount.
function coinChange(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (coin <= i) dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}The detail: a greedy 'always take the biggest coin' approach fails on coin sets like [1, 3, 4] for amount 6 — checking dp[i - coin] + 1 against every coin is what makes this correct where greedy is not.
Q72
Min-cost climbing stairs.
function minCostClimbingStairs(cost) {
let prev = 0, prev2 = 0;
for (let i = 2; i <= cost.length; i++) {
const cur = Math.min(prev + cost[i - 1], prev2 + cost[i - 2]);
[prev2, prev] = [prev, cur];
}
return prev;
}The detail: only the previous two results are ever needed, so this collapses an O(n) DP array down to two variables — the same space-optimization trick that turns the Level 1 Fibonacci loop into O(1) space.
Q73
Detect a cycle in an object graph.
function hasCycle(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== "object") return false;
if (seen.has(obj)) return true;
seen.add(obj);
return Object.values(obj).some(val => hasCycle(val, seen));
}
const a = {}; a.self = a;
hasCycle(a); // trueThe detail: a WeakSet instead of a Set means objects visited during the check are not held in memory afterward — the same reasoning as using a WeakMap for private state in Level 9.
Q74
What prints? A closure capturing an object vs a primitive inside a loop.
const fns = [];
const obj = { count: 0 };
for (let i = 0; i < 3; i++) {
obj.count = i;
fns.push(() => obj.count);
}
console.log(fns.map(fn => fn()));
// [2, 2, 2] — not [0, 1, 2]Why: let gives each iteration its own binding for i, but obj is the same object every time — all three closures read the same obj.count, which holds whatever it was last set to. This is a closure trap the var-vs-let question in Level 6 does not cover.
Q75
What prints? A Proxy get trap during destructuring.
const logged = new Proxy({ a: 1, b: 2 }, {
get(target, key) {
console.log("read:", key);
return target[key];
}
});
const { a, b } = logged;
// logs "read: a" then "read: b"Why: destructuring reads each property individually through the ordinary property-access mechanism, so the Proxy's get trap fires once per property in declaration order — the same as writing logged.a and logged.b by hand.
Q76
What prints? Calling .return() on a generator early.
function* gen() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("cleanup");
}
}
const it = gen();
console.log(it.next().value); // 1
console.log(it.return(99)); // logs "cleanup", then { value: 99, done: true }Why: calling .return() forces the generator to exit as if a return statement ran at the current yield, which is why the finally block executes immediately — the same guarantee an ordinary try/finally gives on an early return.
Q77
What breaks? Using await inside Array.prototype.forEach instead of for...of.
async function run(ids) {
ids.forEach(async id => {
await sleep(10);
console.log("forEach:", id);
});
console.log("after forEach");
for (const id of ids) {
await sleep(10);
console.log("for...of:", id);
}
}
// "after forEach" logs immediately, before any forEach callback resolvesWhy: forEach does not know its callback is async — it fires all callbacks immediately and ignores the promises they return, so 'after forEach' logs before any of them settle. for...of actually awaits each iteration, which is why it is the correct choice whenever order matters.
Q78
Extend debounce with cancel() and flush() methods.
function debounce(fn, delay) {
let timer, lastArgs, lastThis;
function debounced(...args) {
lastArgs = args; lastThis = this;
clearTimeout(timer);
timer = setTimeout(() => { fn.apply(lastThis, lastArgs); timer = null; }, delay);
}
debounced.cancel = () => { clearTimeout(timer); timer = null; };
debounced.flush = () => {
if (timer) { clearTimeout(timer); fn.apply(lastThis, lastArgs); timer = null; }
};
return debounced;
}The detail: cancel() and flush() both need to read the same lastArgs/lastThis the pending call would have used — without capturing them outside setTimeout, flush() would have nothing to call fn with.
Level 11 — Scenario-Based Questions (Real Production Bugs)
Most real interviews do not stop at logic puzzles past the first round. They describe a symptom — a UI that freezes, a memory leak, an error that never reaches your logs — and watch how you reason through it. No code to write here; you have to diagnose the cause.
Q79
A UI freezes for two seconds when a button is clicked, even though the click handler looks lightweight. How do you find the cause?
The detail: JavaScript runs on a single thread, so any synchronous heavy work inside the handler — a big JSON.parse, a nested loop over a large array, a blocking regex — blocks rendering until it finishes. Profile with the Chrome DevTools Performance tab to find the long task, then move the work off the main thread with a Web Worker, or break it into chunks scheduled with setTimeout or requestIdleCallback.
Q80
Two setTimeout(fn, 0) calls fire in an unexpected order under heavy load. Is this a bug?
The detail: No. setTimeout only guarantees a minimum delay, never an exact one — if the call stack or the microtask queue is busy, queued macrotasks simply wait their turn and run whenever the event loop is actually free. Ordering under load is expected, not a defect.
Q81
A search box fires an API call on every keystroke, and users see results flicker as older responses resolve after newer ones. 'Just add debounce' fixed the request rate but not the flicker. What's still missing?
The detail: Debouncing reduces how often you call the API, but a slow older request can still resolve after a faster newer one. Cancel the previous in-flight request with an AbortController when a new one starts, or tag each request with an incrementing id and ignore any response whose id is not the latest.
Q82
A single-page app's memory grows every time a component mounts and unmounts, even though no new DOM nodes are visibly left behind. How do you investigate?
The detail: Take two heap snapshots in the browser's Memory tab, minutes apart, and diff them. Look for detached DOM nodes or growing counts of a specific object type — the usual cause is a closure formed inside the component (an event listener, an interval, a subscription) that captures the component's scope and is never torn down, so neither the closure nor what it references can be garbage-collected.
Q83
An app polls an API with setInterval, and after a user navigates away and back several times in a single-page app, you see duplicate overlapping network calls. What happened?
The detail: The interval from the previous mount was never cleared before the component mounted again, so multiple intervals are now running concurrently against the same endpoint. Always store the interval id and call clearInterval in the teardown path tied to that exact mount, not just at some later convenient point.
Q84
A class method passed directly as a callback — e.g. to setTimeout or addEventListener — throws 'Cannot read properties of undefined' in production. What are your options, and which do you prefer in a class-based codebase?
The detail: Three fixes: bind it in the constructor (this.method = this.method.bind(this)), define it as an arrow function class field so it captures the instance's this at creation, or wrap it in an inline arrow function at the call site. Arrow class fields are usually the cleanest default in a large codebase — the binding is declared once, next to the method, instead of scattered across every call site.
Q85
Extracting a method off an object — const fn = obj.method; fn() — throws or behaves differently than calling obj.method() directly. Why?
The detail: A method's this is determined by how it is called, not where it is defined. obj.method() binds this to obj at the call site; assigning the function reference to a bare variable strips that context, so calling fn() runs with this as undefined (in strict mode) or the global object. Only an arrow function or an explicitly bound function keeps working after extraction.
Q86
A function is supposed to work on a copy of a user object, but the caller's original object changes too. What's happening, and what's the general fix?
The detail: Objects and arrays are assigned and passed by reference, so const copy = user; copy.name = 'x' mutates the exact same object user points to — there was never a second object. Use a spread ({ ...user }), Object.assign({}, user), or structuredClone(user) for a deep copy, depending on how much of the object's structure needs to be independent.
Q87
In a Redux-style store, a reducer mutates state directly instead of returning a new object, and the UI silently does not re-render. Why does that specific bug produce that specific symptom?
The detail: Most state-driven UI libraries decide whether to re-render by comparing the previous state reference to the new one, not by deep-comparing their contents. Mutating the existing object in place means the reference never changes, so the comparison sees 'no change' even though the data inside genuinely changed. The fix is always to return a new object or array, never to mutate the one you were given.
Q88
An async function's try/catch does not catch an error thrown inside a .then() callback chained after an await. Why?
The detail: Mixing await with a separate, un-returned .then() chain inside the same function creates a promise that runs outside the try block's supervision — if that .then() callback throws, the outer async function has typically already moved past the try/catch by the time it happens. Keep the flow consistently on await, or explicitly return the .then() chain so its rejection stays inside the function the try/catch is guarding.
Q89
An async function silently swallows an error — nothing appears in the console or your error-tracking tool. What's the likely cause, and how do you prevent this across a whole codebase?
The detail: Almost always a fire-and-forget async call: something invoked an async function without awaiting or .catch()-ing it, so its rejection has nowhere to go and becomes an unhandled promise rejection. Add a global window.addEventListener('unhandledrejection', ...) (or process.on('unhandledRejection', ...) in Node) as a safety net, and enable an ESLint rule like no-floating-promises to catch the missing await at review time instead of in production.
Q90
A scroll handler that updates layout on every scroll event causes visible jank on mobile. You already tried throttling by time — why might that still not be smooth?
The detail: A time-based throttle can still fire mid-frame, forcing a layout read or write at a moment the browser was not about to repaint anyway, which causes layout thrashing. Batch the update inside requestAnimationFrame instead, so the DOM write happens right before the browser's own paint — aligned with the render cycle rather than an arbitrary interval.
Q91
A form's submit handler calls e.preventDefault(), but the page still reloads intermittently in production. What are the likely causes?
The detail: Three usual suspects: an error thrown earlier in the handler before preventDefault() executes, so the call never happens; a second submit listener attached elsewhere on the same form that does not call preventDefault(); or event delegation misconfigured so the handler is bound to the wrong element and never actually intercepts the real submit event.
Q92
Sorting an array of numbers with Array.prototype.sort() gives an obviously wrong order — [10, 1, 2] comes back as [1, 10, 2]. Why, and how do you fix it?
The detail: sort() converts elements to strings and compares them lexicographically by default, so '10' sorts before '2' because '1' < '2' as characters. Pass an explicit comparator — arr.sort((a, b) => a - b) — whenever you are sorting numbers, not the default.
Q93
A check like if (user.isPremium) throws 'Cannot read properties of undefined' in production, but only for some users, and it never reproduces locally.
The detail: This is almost always a timing issue: user is briefly null or undefined because the data has not finished loading yet, or the API returned a partial response for that user. Guard with optional chaining (user?.isPremium) and make sure the UI has an explicit loading or empty state instead of assuming the data is always present by the time this line runs.
Q94
JSON.stringify(obj) is missing fields when you log an object for debugging, even though the fields clearly exist when you inspect the object directly.
The detail: JSON.stringify silently drops properties whose value is a function, undefined, or a Symbol, and it will call a custom toJSON() method on any nested object that defines one (Date is the classic example) and use its return value instead of the object's own properties. Check for a toJSON() override on nested objects before assuming the data itself is missing.
Q95
A value read from process.env at the top of a module is undefined in production but works fine locally. What in the module loading order likely broke it?
The detail: Imports and requires execute top-to-bottom, eagerly, at load time — if the module that reads process.env.SOME_KEY is imported before your env-loading step (like dotenv.config()) has actually run, it captures undefined and keeps it, because the top-level read only happens once. Load environment configuration before importing anything that depends on it, or read the variable lazily inside a function instead of at module scope.
Q96
Two different files import what should be the same module, but each gets its own separate instance of a singleton 'cache' object, despite Node caching modules by default.
The detail: Node's module cache is keyed by the resolved absolute file path, not by package name — if the same package exists twice in node_modules (a version mismatch between dependencies, or a monorepo package resolved through two different symlink paths), each resolves to a different file path and therefore a different cached instance. Check the actual resolved paths with something like npm ls or node --trace-resolve rather than assuming the import statements alone guarantee one instance.
The 2 Mistakes That Fail Most Candidates
- Jumping straight to the clever one-liner. A candidate who writes [...new Set(arr)] and cannot explain what Set does with NaN, objects or -0 looks like someone who memorized a snippet. Solve it with a loop first, then compress. The loop is your proof of understanding.
- Never testing the empty and negative cases. Empty array, empty string, single element, negative number, duplicate values. These five inputs catch the majority of "almost correct" answers. Run them before you say "done" — in an interview and in production.
A 65-Day Practice Plan
- Days 1–6: Level 1 and 2. One question per day, written twice — once with a loop, once with built-in methods.
- Days 7–14: Level 3. Arrays carry the most interview weight. Add map, filter, reduce rewrites of every solution.
- Days 15–20: Level 4. Closures, currying, debounce, throttle and memoize — these appear in almost every frontend interview above the fresher level.
- Days 21–26: Level 5. Objects, plus one small real utility a day (a config flattener, a group-by report).
- Days 27–30: Level 6, then redo five questions from Level 1 without looking.
- Days 31–35: Level 7. Design patterns and utilities — pub/sub, bind, compose/pipe, LRU cache and a mini-Promise. These are what separates a candidate from someone who has only used libraries.
- Days 36–40: Level 8. Stacks, queues, linked lists, binary search and sorting.
- Days 41–48: Level 9. Generators, Proxy, WeakMap and async concurrency patterns — this is where mid-to-senior interviews actually differentiate candidates.
- Days 49–55: Level 10. Dynamic programming and the tricky output questions.
- Days 56–65: Level 11. Scenario-based production bugs — the event loop, memory leaks, this binding, and async errors that never reach your logs. Most real interviews above the fresher level live in this level, not in the algorithm rounds. If you can diagnose every one of these out loud, you are ready for the interview.
Track it in a single repo with one file per question and a comment at the top stating the time complexity. That repo becomes a portfolio artifact by day 65 — far more convincing than a certificate.
FAQs
How many logic questions are enough for a JavaScript interview?
Around 40-50 solved properly, with edge cases and complexity understood, beats 300 half-remembered ones. Depth matters more than the count. The 96 here cover every pattern that repeats, including the design-pattern, metaprogramming, dynamic-programming and real production-scenario questions that start showing up above the fresher level.
Should I use built-in methods or write the loop manually?
Both. Interviewers test whether you know reduce, and separately whether you could implement it. Practice writing the loop first and the method version second.
Do I need DSA for a JavaScript developer job?
For most product and service companies in India: basic arrays, strings, hash maps, and recursion. Graph and dynamic-programming rounds appear mainly at product companies and large tech firms. Finish this list before starting DSA.
Is it a problem that I don't have a computer science degree?
No. Non-CS backgrounds are common in JavaScript roles. What replaces the degree is a public repo of solved problems plus two or three deployed projects — that evidence is checkable, and a degree is not.
What should I learn immediately after logic building?
The event loop and async in depth, then one framework end to end (React or Angular), then Git and deployment. Logic gets you through the interview; project structure keeps you in the job.
Final Thoughts
Logic building is not about memorizing 96 answers — it is about recognizing the handful of patterns hiding underneath them. Once two-pointer, sliding-window, frequency-map, closure-based state, generator-based laziness, DP tables and the instinct to ask "why does this break in production" click, new problems stop feeling new. Build that recognition with timed mock interviews at Roundexa.com.
Ready to Practice?
Take a free AI mock interview on Roundexa and get instant, actionable feedback before the real one.
Practice on RoundexaRead Next
Java Interview Questions and Answers for Freshers 2026
Java interviews follow patterns. This guide covers the questions that come up most — OOP, Collections, Exception Handling, multithreading — with answers that are easy to understand and explain.
Angular Coding Interview Questions for Experienced Developers (6+ Years) 2026
10 hands-on Angular coding exercises for senior developers — custom RxJS operators, structural directives, pure pipes, DI tokens, and building a state store from scratch, each with a solution.