Master advanced JavaScript with 15+ real interview-level problems covering promises, async patterns, event loop, prototypes, generators, proxy, WeakMap, design patterns, throttle/debounce, promise pool, retry with backoff, deep equality, and pub/sub systems.
This problem set contains 15+ interview-level challenges designed to test deep understanding of JavaScript internals, async patterns, metaprogramming, and software design. Each problem includes a clear statement, a working solution, and the expected output.
Problem 2 — Promise.all with Error Handling
Statement: Implement a function settleAll that behaves like Promise.allSettled — it never rejects and returns the status of each promise.
function settleAll(promises) {
return Promise.all(
promises.map((p) =>
Promise.resolve(p)
.then((value) => ({ status: "fulfilled", value }))
.catch((reason) => ({ status: "rejected", reason }))
)
);
}
// Test
const promises = [
Promise.resolve(10),
Promise.reject("network error"),
Promise.resolve(30),
];
settleAll(promises).then((results) => {
results.forEach((r) => console.log(r.status, r.value || r.reason));
});
Solution
By wrapping each promise in a .then/.catch that always resolves, Promise.all never short-circuits. Every result is captured regardless of individual failures.
fulfilled 10
rejected network error
fulfilled 30
Problem 3 — Promise Pool (Concurrency Limiter)
Statement: Implement promisePool(tasks, concurrency) that runs async tasks with a maximum of concurrency simultaneous executions.
async function promisePool(tasks, concurrency) {
const results = [];
let index = 0;
async function worker() {
while (index < tasks.length) {
const currentIndex = index++;
results[currentIndex] = await tasks[currentIndex]();
}
}
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());
await Promise.all(workers);
return results;
}
// Test
const tasks = [
() => new Promise((r) => setTimeout(() => r("A done"), 100)),
() => new Promise((r) => setTimeout(() => r("B done"), 50)),
() => new Promise((r) => setTimeout(() => r("C done"), 80)),
() => new Promise((r) => setTimeout(() => r("D done"), 30)),
() => new Promise((r) => setTimeout(() => r("E done"), 60)),
];
promisePool(tasks, 2).then((results) => {
console.log("All complete:", results);
});
Solution
Workers compete for the next available task index. At most concurrency workers run in parallel, each picking up a new task as soon as it finishes one.
All complete: [ 'A done', 'B done', 'C done', 'D done', 'E done' ]
Problem 4 — Retry with Exponential Backoff
Statement: Write retryWithBackoff(fn, retries, baseDelay) that retries an async function with exponentially increasing delays.
function retryWithBackoff(fn, retries = 3, baseDelay = 1000) {
return new Promise((resolve, reject) => {
function attempt(n) {
fn()
.then(resolve)
.catch((err) => {
if (n <= 0) {
reject(err);
} else {
const delay = baseDelay * Math.pow(2, retries - n);
console.log(`Retry in ${delay}ms... (${n} left)`);
setTimeout(() => attempt(n - 1), delay);
}
});
}
attempt(retries);
});
}
// Simulate an unreliable API
let callCount = 0;
const unreliableAPI = () =>
new Promise((resolve, reject) => {
callCount++;
if (callCount < 3) reject(new Error(`Fail #${callCount}`));
else resolve("Success on attempt " + callCount);
});
retryWithBackoff(unreliableAPI, 3, 100)
.then((val) => console.log("Result:", val))
.catch((err) => console.log("Final error:", err.message));
Solution
Each failure doubles the delay. The function succeeds on the third attempt since the simulated API fails twice then resolves.
Retry in 100ms... (3 left)
Retry in 200ms... (2 left)
Result: Success on attempt 3
Problem 5 — Prototype Chain & Inheritance
Statement: Predict the output by tracing the prototype chain manually.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound.`;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
return `${this.name} barks.`;
};
const rex = new Dog("Rex", "Labrador");
console.log(rex.speak());
console.log(rex instanceof Dog);
console.log(rex instanceof Animal);
console.log(Object.getPrototypeOf(rex) === Dog.prototype);
console.log(rex.hasOwnProperty("speak"));
console.log(rex.hasOwnProperty("name"));
Solution
Dog.prototype has its own speak, so it shadows Animal.prototype.speak. The instanceof checks walk the prototype chain. speak lives on the prototype, not the instance.
Rex barks.
true
true
true
false
true
Problem 6 — Generator-Based Infinite Sequence
Statement: Create a generator that yields Fibonacci numbers infinitely. Use it to get the first 10 Fibonacci numbers.
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function take(gen, n) {
const results = [];
for (const val of gen) {
results.push(val);
if (results.length === n) break;
}
return results;
}
const fib = fibonacci();
console.log("First 10 Fibonacci:", take(fib, 10));
console.log("Next value:", fib.next().value);
Solution
The generator suspends at each yield and resumes when .next() is called. After take consumes 10 values, the generator is still alive at its current state.
First 10 Fibonacci: [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ]
Next value: 55
Problem 7 — Proxy-Based Validation
Statement: Use Proxy to create an object that validates property types on assignment and logs every access.
function createValidated(schema) {
const data = {};
return new Proxy(data, {
set(target, prop, value) {
if (!(prop in schema)) {
throw new Error(`Unknown property: ${prop}`);
}
if (typeof value !== schema[prop]) {
throw new TypeError(
`Expected ${schema[prop]} for "${prop}", got ${typeof value}`
);
}
target[prop] = value;
console.log(`SET ${prop} = ${JSON.stringify(value)}`);
return true;
},
get(target, prop) {
console.log(`GET ${prop}`);
return target[prop];
},
});
}
const user = createValidated({ name: "string", age: "number", active: "boolean" });
user.name = "Alice";
user.age = 28;
user.active = true;
console.log(user.name);
try {
user.age = "twenty";
} catch (e) {
console.log("Error:", e.message);
}
Solution
The Proxy intercepts set and get traps. Invalid types throw immediately, providing runtime type safety without a compilation step.
SET name = "Alice"
SET age = 28
SET active = true
GET name
Alice
Error: Expected number for "age", got string
Problem 8 — WeakMap for Private Data
Statement: Implement a class with truly private properties using WeakMap. The private data should be inaccessible outside the class.
const _private = new WeakMap();
class SecureAccount {
constructor(owner, balance) {
_private.set(this, { owner, balance, history: [] });
}
deposit(amount) {
const priv = _private.get(this);
priv.balance += amount;
priv.history.push(`+${amount}`);
return this;
}
withdraw(amount) {
const priv = _private.get(this);
if (amount > priv.balance) throw new Error("Insufficient funds");
priv.balance -= amount;
priv.history.push(`-${amount}`);
return this;
}
getStatement() {
const { owner, balance, history } = _private.get(this);
return `${owner}: $${balance} | Transactions: ${history.join(", ")}`;
}
}
const acc = new SecureAccount("Bob", 1000);
acc.deposit(500).withdraw(200);
console.log(acc.getStatement());
console.log("Keys on instance:", Object.keys(acc));
console.log("balance" in acc);
Solution
WeakMap keys are object references. Once the instance is garbage-collected, the private data is too. External code cannot access _private from outside the module.
Bob: $1300 | Transactions: +500, -200
Keys on instance: []
false
Problem 9 — Observer Pattern
Statement: Implement a full Observer pattern with subscribe, unsubscribe, and notify capabilities.
class EventEmitter {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return () => this.off(event, callback); // return unsubscribe function
}
off(event, callback) {
const cbs = this.listeners.get(event);
if (cbs) {
this.listeners.set(event, cbs.filter((cb) => cb !== callback));
}
}
emit(event, ...args) {
const cbs = this.listeners.get(event) || [];
cbs.forEach((cb) => cb(...args));
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
const bus = new EventEmitter();
bus.on("data", (msg) => console.log("Listener A:", msg));
const unsub = bus.on("data", (msg) => console.log("Listener B:", msg));
bus.once("data", (msg) => console.log("Once listener:", msg));
bus.emit("data", "first");
console.log("---");
unsub();
bus.emit("data", "second");
Solution
on returns an unsubscribe function for ergonomic cleanup. once wraps the callback and self-removes after first invocation.
Listener A: first
Listener B: first
Once listener: first
---
Listener A: second
Problem 10 — Singleton Pattern with Closure
Statement: Implement a Singleton that lazily initializes and always returns the same instance.
const Database = (() => {
let instance = null;
class _Database {
constructor() {
this.connections = 0;
this.logs = [];
}
connect() {
this.connections++;
this.logs.push(`Connection #${this.connections}`);
return this;
}
getStatus() {
return { connections: this.connections, logs: [...this.logs] };
}
}
return {
getInstance() {
if (!instance) {
instance = new _Database();
console.log("New instance created");
}
return instance;
},
};
})();
const db1 = Database.getInstance();
const db2 = Database.getInstance();
db1.connect().connect();
db2.connect();
console.log("Same instance?", db1 === db2);
console.log("Status:", db2.getStatus());
Solution
The IIFE closure hides the instance variable. getInstance creates the object only on first call. All subsequent calls return the same reference.
New instance created
Same instance? true
Status: { connections: 3, logs: [ 'Connection #1', 'Connection #2', 'Connection #3' ] }
Problem 11 — Throttle & Debounce from Scratch
Statement: Implement both throttle and debounce with proper edge handling.
function throttle(fn, delay) {
let lastCall = 0;
let timeoutId = null;
return function (...args) {
const now = Date.now();
const remaining = delay - (now - lastCall);
if (remaining <= 0) {
lastCall = now;
fn.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastCall = Date.now();
timeoutId = null;
fn.apply(this, args);
}, remaining);
}
};
}
function debounce(fn, delay) {
let timeoutId = null;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Simulation
const log = [];
const throttled = throttle((x) => log.push(`T:${x}`), 100);
const debounced = debounce((x) => log.push(`D:${x}`), 100);
throttled("a");
throttled("b");
throttled("c");
debounced("x");
debounced("y");
debounced("z");
setTimeout(() => {
throttled("d");
console.log("After 150ms:", log);
}, 150);
setTimeout(() => {
console.log("After 300ms:", log);
}, 300);
Solution
Throttle fires immediately on first call, then at most once per delay. Debounce resets the timer on every call, firing only after the input settles.
After 150ms: [ 'T:a', 'T:c', 'D:z', 'T:d' ]
After 300ms: [ 'T:a', 'T:c', 'D:z', 'T:d' ]
Problem 12 — Deep Equality Check
Statement: Implement deepEqual(a, b) that handles objects, arrays, dates, RegExp, null, NaN, and circular references.
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true;
if (a === null || b === null) return false;
if (typeof a !== "object" || typeof b !== "object") return false;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (a instanceof RegExp && b instanceof RegExp) return a.toString() === b.toString();
if (a.constructor !== b.constructor) return false;
// Circular reference check
if (seen.has(a)) return seen.get(a) === b;
seen.set(a, b);
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => deepEqual(a[key], b[key], seen));
}
// Tests
console.log(deepEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] }));
console.log(deepEqual({ a: 1 }, { a: "1" }));
console.log(deepEqual(NaN, NaN));
console.log(deepEqual(new Date("2026-01-01"), new Date("2026-01-01")));
console.log(deepEqual(/abc/gi, /abc/gi));
console.log(deepEqual([1, [2, [3]]], [1, [2, [3]]]));
// Circular reference
const x = { val: 1 };
const y = { val: 1 };
x.self = x;
y.self = y;
console.log(deepEqual(x, y));
Solution
Object.is handles NaN === NaN and +0 !== -0. The WeakMap tracks seen objects to safely handle circular structures without infinite recursion.
true
false
true
true
true
true
true
Problem 13 — Custom JSON.stringify
Statement: Implement a simplified jsonStringify that handles primitives, arrays, objects, null, undefined, and functions (omitting them from objects).
function jsonStringify(value) {
if (value === null) return "null";
if (value === undefined || typeof value === "function") return undefined;
const type = typeof value;
if (type === "number") {
return isFinite(value) ? String(value) : "null";
}
if (type === "boolean") return String(value);
if (type === "string") return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
if (Array.isArray(value)) {
const items = value.map((item) => {
const serialized = jsonStringify(item);
return serialized === undefined ? "null" : serialized;
});
return `[${items.join(",")}]`;
}
if (type === "object") {
if (typeof value.toJSON === "function") {
return jsonStringify(value.toJSON());
}
const pairs = [];
for (const key of Object.keys(value)) {
const serialized = jsonStringify(value[key]);
if (serialized !== undefined) {
pairs.push(`"${key}":${serialized}`);
}
}
return `{${pairs.join(",")}}`;
}
return undefined;
}
// Tests
console.log(jsonStringify({ a: 1, b: "hello", c: true }));
console.log(jsonStringify([1, null, undefined, "test"]));
console.log(jsonStringify({ fn: () => {}, val: 42 }));
console.log(jsonStringify(Infinity));
console.log(jsonStringify("line\nnew"));
// Compare with native
const obj = { x: [1, 2], y: { nested: true } };
console.log(jsonStringify(obj) === JSON.stringify(obj));
Solution
Functions and undefined are omitted from objects but become null in arrays. Infinity and NaN serialize as "null" per the JSON spec.
{"a":1,"b":"hello","c":true}
[1,null,null,"test"]
{"val":42}
null
"line\nnew"
true
Problem 14 — Pub/Sub System with Wildcard Support
Statement: Build a publish/subscribe system that supports wildcard topic matching (e.g., user.* matches user.login and user.logout).
class PubSub {
constructor() {
this.subscribers = new Map();
}
subscribe(pattern, callback) {
if (!this.subscribers.has(pattern)) {
this.subscribers.set(pattern, new Set());
}
this.subscribers.get(pattern).add(callback);
return () => this.subscribers.get(pattern)?.delete(callback);
}
publish(topic, data) {
let notified = 0;
for (const [pattern, callbacks] of this.subscribers) {
if (this._matches(pattern, topic)) {
callbacks.forEach((cb) => {
cb(topic, data);
notified++;
});
}
}
return notified;
}
_matches(pattern, topic) {
if (pattern === topic) return true;
if (pattern === "*") return true;
const regex = new RegExp(
"^" + pattern.replace(/\./g, "\\.").replace(/\*/g, "[^.]+") + "$"
);
return regex.test(topic);
}
}
const ps = new PubSub();
ps.subscribe("user.*", (topic, data) => console.log(`[user.*] ${topic}:`, data));
ps.subscribe("user.login", (topic, data) => console.log(`[exact] ${topic}:`, data));
ps.subscribe("*", (topic, data) => console.log(`[global] ${topic}:`, data));
console.log("Notified:", ps.publish("user.login", { id: 1 }));
console.log("---");
console.log("Notified:", ps.publish("user.logout", { id: 1 }));
console.log("---");
console.log("Notified:", ps.publish("system.error", { code: 500 }));
Solution
The wildcard * matches any single segment. A standalone * pattern matches everything. Pattern matching uses regex conversion for flexibility.
[user.*] user.login: { id: 1 }
[exact] user.login: { id: 1 }
[global] user.login: { id: 1 }
Notified: 3
---
[user.*] user.logout: { id: 1 }
[global] user.logout: { id: 1 }
Notified: 2
---
[global] system.error: { code: 500 }
Notified: 1
Problem 15 — Factory Pattern with Registry
Statement: Implement a factory that registers creators and produces objects by type, supporting plugins added at runtime.
class ShapeFactory {
constructor() {
this.registry = new Map();
}
register(type, creator) {
this.registry.set(type, creator);
return this;
}
create(type, ...args) {
const creator = this.registry.get(type);
if (!creator) throw new Error(`Unknown shape: ${type}`);
return creator(...args);
}
getTypes() {
return [...this.registry.keys()];
}
}
const factory = new ShapeFactory();
factory
.register("circle", (r) => ({ type: "circle", area: Math.PI * r * r }))
.register("rectangle", (w, h) => ({ type: "rectangle", area: w * h }))
.register("triangle", (b, h) => ({ type: "triangle", area: 0.5 * b * h }));
console.log(factory.create("circle", 5));
console.log(factory.create("rectangle", 4, 6));
console.log(factory.create("triangle", 3, 8));
console.log("Available:", factory.getTypes());
// Runtime plugin
factory.register("hexagon", (s) => ({ type: "hexagon", area: (3 * Math.sqrt(3) / 2) * s * s }));
console.log(factory.create("hexagon", 4).area.toFixed(2));
Solution
The registry pattern decouples creation from usage. New shapes can be added at runtime without modifying existing code — following the Open/Closed principle.
{ type: 'circle', area: 78.53981633974483 }
{ type: 'rectangle', area: 24 }
{ type: 'triangle', area: 12 }
Available: [ 'circle', 'rectangle', 'triangle' ]
41.57
Statement: Create an async generator that fetches paginated data and yields items one at a time, abstracting away pagination logic.
async function* paginate(fetchPage) {
let page = 1;
let hasMore = true;
while (hasMore) {
const { data, nextPage } = await fetchPage(page);
for (const item of data) {
yield item;
}
hasMore = nextPage !== null;
page = nextPage;
}
}
// Simulated API
const mockAPI = async (page) => {
const pages = {
1: { data: ["item1", "item2", "item3"], nextPage: 2 },
2: { data: ["item4", "item5"], nextPage: 3 },
3: { data: ["item6"], nextPage: null },
};
return pages[page];
};
(async () => {
const items = [];
for await (const item of paginate(mockAPI)) {
items.push(item);
}
console.log("All items:", items);
console.log("Count:", items.length);
})();
Solution
The async generator lazily fetches pages only when consumers request more items. The for await...of loop handles the async iteration protocol automatically.
All items: [ 'item1', 'item2', 'item3', 'item4', 'item5', 'item6' ]
Count: 6
Problem 17 — Memoize with TTL and Size Limit
Statement: Implement an advanced memoization function with time-to-live expiration and maximum cache size (LRU eviction).
function memoizeAdvanced(fn, { maxSize = 100, ttl = Infinity } = {}) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const entry = cache.get(key);
if (Date.now() - entry.timestamp < ttl) {
// Move to end (most recently used)
cache.delete(key);
cache.set(key, entry);
return entry.value;
}
cache.delete(key); // Expired
}
const value = fn.apply(this, args);
if (cache.size >= maxSize) {
// Delete oldest (first) entry - LRU eviction
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, { value, timestamp: Date.now() });
return value;
};
}
// Test
let callCount = 0;
const expensive = (n) => {
callCount++;
return n * n;
};
const memoized = memoizeAdvanced(expensive, { maxSize: 3, ttl: 5000 });
console.log(memoized(4)); // computes
console.log(memoized(4)); // cached
console.log(memoized(5)); // computes
console.log(memoized(6)); // computes
console.log(memoized(7)); // computes, evicts key for 4
console.log(memoized(4)); // computes again (was evicted)
console.log("Total calls:", callCount);
Solution
Map preserves insertion order, enabling LRU eviction by deleting the first key. TTL check ensures stale entries are recomputed.
16
16
25
36
49
16
Total calls: 5
Problem 18 — Currying with Placeholder Support
Statement: Implement a curry function that supports partial application and a placeholder _ for skipping arguments.
const _ = Symbol("placeholder");
function curry(fn) {
const arity = fn.length;
return function curried(...args) {
// Filter out placeholders to count real args
const realArgs = args.filter((a) => a !== _);
if (realArgs.length >= arity && !args.slice(0, arity).includes(_)) {
return fn(...args.slice(0, arity));
}
return function (...nextArgs) {
const merged = [];
let nextIdx = 0;
for (const arg of args) {
if (arg === _ && nextIdx < nextArgs.length) {
merged.push(nextArgs[nextIdx++]);
} else {
merged.push(arg);
}
}
// Append remaining nextArgs
while (nextIdx < nextArgs.length) {
merged.push(nextArgs[nextIdx++]);
}
return curried(...merged);
};
};
}
const add3 = curry((a, b, c) => a + b + c);
console.log(add3(1)(2)(3));
console.log(add3(1, 2)(3));
console.log(add3(1, 2, 3));
console.log(add3(_, 2, 3)(1));
console.log(add3(_, _, 3)(1)(2));
console.log(add3(_, 2, _)(1, 3));
Solution
Placeholders are tracked by position. When new arguments arrive, they fill placeholder slots left-to-right before appending to the end.
Problem 19 — Event Loop Deep Dive: async/await Ordering
Statement: Predict the exact output. This tests understanding of how await desugars to .then() and how control flows between async functions.
async function first() {
console.log("1");
await second();
console.log("2");
}
async function second() {
console.log("3");
await Promise.resolve();
console.log("4");
}
console.log("5");
first();
console.log("6");
Promise.resolve().then(() => console.log("7"));
Solution
await splits the function at that point — everything after becomes a microtask. Each await introduces one microtask boundary.
- Sync: "5", then
first() starts → "1", second() starts → "3", suspends at await Promise.resolve(). Back in first, suspends at await second(). Sync continues: "6". - Microtask queue: "4" (second resumes), "7" (Promise.resolve callback), "2" (first resumes after second completes).
Problem 20 — Reactive State with Proxy (Mini Reactivity System)
Statement: Build a minimal reactive system where accessing properties tracks dependencies and setting properties triggers effects.
let activeEffect = null;
function reactive(obj) {
const deps = new Map();
return new Proxy(obj, {
get(target, key) {
if (activeEffect) {
if (!deps.has(key)) deps.set(key, new Set());
deps.get(key).add(activeEffect);
}
return target[key];
},
set(target, key, value) {
target[key] = value;
const effects = deps.get(key);
if (effects) effects.forEach((fn) => fn());
return true;
},
});
}
function effect(fn) {
activeEffect = fn;
fn(); // Run once to collect dependencies
activeEffect = null;
}
// Test
const state = reactive({ count: 0, name: "Quick" });
const log = [];
effect(() => {
log.push(`Count is: ${state.count}`);
});
effect(() => {
log.push(`Name is: ${state.name}`);
});
state.count = 1;
state.count = 2;
state.name = "WoHo";
console.log(log);
Solution
On first run, effect sets itself as activeEffect. Property access inside the function registers the effect as a dependency. Subsequent sets trigger only the relevant effects.
[
'Count is: 0',
'Name is: Quick',
'Count is: 1',
'Count is: 2',
'Name is: WoHo'
]
Summary
These 20 problems cover the advanced JavaScript territory expected in senior-level interviews:
| # | Topic | Key Concept |
|---|
| 1 | Event Loop | Microtasks vs Macrotasks |
| 2 | Promise.allSettled | Error isolation |
| 3 | Promise Pool | Concurrency control |
| 4 | Retry + Backoff | Fault tolerance |
| 5 | Prototypes | Inheritance chain |
| 6 | Generators | Lazy evaluation |
| 7 | Proxy | Metaprogramming |
| 8 | WeakMap | True privacy |
| 9 | Observer | Event-driven design |
| 10 | Singleton | Instance control |
| 11 | Throttle/Debounce | Rate limiting |
| 12 | Deep Equality | Recursive comparison |
| 13 | JSON.stringify | Serialization |
| 14 | Pub/Sub | Wildcard messaging |
| 15 | Factory | Plugin architecture |
| 16 | Async Generator | Lazy async iteration |
| 17 | Memoization | Cache with TTL + LRU |
| 18 | Currying | Placeholder partial application |
| 19 | async/await | Execution ordering |
| 20 | Reactivity | Dependency tracking |
Practice each problem by predicting output first, then verifying. Aim to explain your reasoning as if in a live interview setting.