Implement a simple
EventEmitterwithon,off,onceandemit.Keep a
Mapfrom event name to an array of listeners.JavaScriptclass MiniEmitter { #listeners = new Map(); on(event, fn) { if (!this.#listeners.has(event)) this.#listeners.set(event, []); this.#listeners.get(event).push(fn); return this; } off(event, fn) { const list = this.#listeners.get(event) ?? []; this.#listeners.set(event, list.filter((l) => l !== fn && l.original !== fn)); return this; } once(event, fn) { const wrapper = (...args) => { this.off(event, wrapper); fn.apply(this, args); }; wrapper.original = fn; return this.on(event, wrapper); } emit(event, ...args) { const list = this.#listeners.get(event); if (!list || list.length === 0) { if (event === 'error') throw args[0]; return false; } for (const fn of [...list]) fn.apply(this, args); return true; } } const e = new MiniEmitter(); const greet = (name) => console.log('hello', name); e.on('join', greet); e.once('join', (name) => console.log('first join was', name)); e.emit('join', 'Ada'); e.emit('join', 'Linus'); e.off('join', greet); console.log(e.emit('join', 'nobody'));Details that get checked:
emitloops over a copy, so aoncelistener removing itself does not skip the next one, andofffindsoncewrappers throughoriginal.Write your own
promisify.Return a function that creates a promise and calls the original with an extra error-first callback that settles it.
JavaScriptfunction promisify(fn) { return function (...args) { return new Promise((resolve, reject) => { fn.call(this, ...args, (err, value) => { if (err) reject(err); else resolve(value); }); }); }; } function readConfig(name, callback) { setTimeout(() => { if (name !== 'app') return callback(new Error(`no config named ${name}`)); callback(null, { port: 8080 }); }, 1); } const readConfigAsync = promisify(readConfig); readConfigAsync('app').then((cfg) => console.log('port', cfg.port)); readConfigAsync('db').catch((err) => console.log(err.message));A regular
function, not an arrow, keepsthisfor methods.Run async tasks with at most N in flight at once (a promise pool).
Start
limitrunners; each takes the next index from a shared counter until none are left.JavaScriptasync function mapLimit(items, limit, worker) { const results = new Array(items.length); let next = 0; async function runner() { while (next < items.length) { const i = next++; results[i] = await worker(items[i], i); } } const runners = Array.from({ length: Math.min(limit, items.length) }, runner); await Promise.all(runners); return results; } const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let active = 0; let peak = 0; async function fetchPage(id) { active++; peak = Math.max(peak, active); await sleep(10 + (id % 3) * 5); active--; return `page ${id}`; } mapLimit([1, 2, 3, 4, 5, 6, 7], 3, fetchPage).then((pages) => { console.log(pages.join(', ')); console.log('max at once:', peak); });next++needs no lock, because JavaScript runs one callback at a time, and storing results by index keeps the input order.Write a function that retries a failing async call with exponential backoff.
Loop over attempts,
awaitthe call insidetry, and on failure waitbase * 2^attempt.JavaScriptconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function retry(fn, { retries = 3, baseMs = 10 } = {}) { for (let attempt = 0; ; attempt++) { try { return await fn(attempt); } catch (err) { if (attempt >= retries) throw err; const delay = baseMs * 2 ** attempt; console.log(`attempt ${attempt + 1} failed (${err.message}), retrying in ${delay} ms`); await sleep(delay); } } } let calls = 0; async function flakyRequest() { calls++; if (calls < 3) throw new Error('ECONNRESET'); return `ok after ${calls} calls`; } retry(flakyRequest).then(console.log);Add random jitter, retry only transient errors (
ECONNRESET,503,429), and never retry a payment without an idempotency key.Implement an LRU cache in JavaScript.
A
Mapkeeps insertion order, so if every read moves its key to the end, the first key is the least recently used.JavaScriptclass LRUCache { constructor(capacity) { this.capacity = capacity; this.map = new Map(); } get(key) { if (!this.map.has(key)) return undefined; const value = this.map.get(key); this.map.delete(key); // move to the newest position this.map.set(key, value); return value; } set(key, value) { if (this.map.has(key)) this.map.delete(key); this.map.set(key, value); if (this.map.size > this.capacity) { const oldest = this.map.keys().next().value; this.map.delete(oldest); } } } const cache = new LRUCache(2); cache.set('a', 1); cache.set('b', 2); cache.get('a'); // a is now the most recent cache.set('c', 3); // evicts b, the least recently used console.log([...cache.map.keys()].join(','), cache.get('b'));Both operations are O(1). The classic version is a hash map plus a doubly linked list.
Find the most frequent words in a large text stream.
Count with a
Mapwhile streaming, and carry over a word cut in half at a chunk boundary.JavaScriptconst { Readable } = require('node:stream'); async function topWords(stream, k) { const counts = new Map(); const add = (w) => counts.set(w, (counts.get(w) ?? 0) + 1); let leftover = ''; for await (const chunk of stream) { const words = (leftover + chunk.toString()).toLowerCase().split(/\s+/); leftover = words.pop(); // the last word may continue in the next chunk for (const w of words) if (w) add(w); } if (leftover) add(leftover); return [...counts] .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) .slice(0, k); } // the chunks split "dog" on purpose, like a real stream can const source = Readable.from(['the cat and the d', 'og and the bird ', 'saw the cat']); topWords(source, 3).then((top) => { for (const [word, n] of top) console.log(word, n); });Without
leftover,dandogcount as two words. For a large vocabulary and small k, use a min-heap of size k: O(n log k).Practice it: Top K Frequent Elements, Kth Largest Element in an Array
Given packages and their dependencies, print an install order and detect cycles.
A topological sort. Kahn's algorithm: start with packages that have no unmet dependencies and release each package once everything it needs is installed.
JavaScriptfunction installOrder(deps) { const pending = new Map(); // package -> number of unmet dependencies const dependents = new Map(); // package -> packages that need it for (const [pkg, needs] of Object.entries(deps)) { pending.set(pkg, (pending.get(pkg) ?? 0) + needs.length); for (const d of needs) { if (!pending.has(d)) pending.set(d, 0); if (!dependents.has(d)) dependents.set(d, []); dependents.get(d).push(pkg); } } const ready = [...pending].filter(([, n]) => n === 0).map(([pkg]) => pkg); const order = []; while (ready.length) { const pkg = ready.shift(); order.push(pkg); for (const next of dependents.get(pkg) ?? []) { pending.set(next, pending.get(next) - 1); if (pending.get(next) === 0) ready.push(next); } } if (order.length !== pending.size) throw new Error('dependency cycle'); return order; } console.log(installOrder({ app: ['express', 'pg'], express: ['body-parser'], 'body-parser': [], pg: [], }).join(' > ')); try { installOrder({ a: ['b'], b: ['a'] }); } catch (err) { console.log(err.message); }O(V + E); leftover packages mean a cycle. See the topological sort visualization.
Practice it: Course Schedule, Alien Dictionary
From request logs with start and end times, find the peak number of concurrent requests.
Sweep the timeline: +1 at each start, -1 at each end, sorted, with a running total.
JavaScriptfunction peakConcurrency(requests) { const events = []; for (const { start, end } of requests) { events.push([start, 1]); events.push([end, -1]); } // at equal times, ends come before starts: a request ending at 30 // does not overlap one starting at 30 events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); let current = 0; let peak = 0; for (const [, change] of events) { current += change; peak = Math.max(peak, current); } return peak; } const log = [ { start: 0, end: 30 }, { start: 5, end: 10 }, { start: 15, end: 20 }, { start: 8, end: 18 }, { start: 30, end: 40 }, ]; console.log(peakConcurrency(log));O(n log n). It is the meeting rooms problem, and it sizes a connection pool from real traffic.
Practice it: Meeting Rooms II, Merge Intervals
Merge two log files that are each sorted by timestamp into one sorted log.
Two pointers: always take the earlier of the two current lines.
JavaScriptfunction mergeLogs(a, b) { const out = []; let i = 0; let j = 0; while (i < a.length && j < b.length) { out.push(a[i].ts <= b[j].ts ? a[i++] : b[j++]); } while (i < a.length) out.push(a[i++]); while (j < b.length) out.push(b[j++]); return out; } const api = [ { ts: 1, msg: 'api start' }, { ts: 4, msg: 'api request' }, { ts: 9, msg: 'api stop' }, ]; const db = [ { ts: 2, msg: 'db connect' }, { ts: 4, msg: 'db query' }, { ts: 7, msg: 'db slow query' }, ]; for (const line of mergeLogs(api, db)) console.log(line.ts, line.msg);<=keeps the merge stable at ties; O(n + m). For k files, keep each file's current line in a min-heap.Practice it: Merge Sorted Array, Merge k Sorted Lists
Implement a rate limiter that allows N requests per time window for each client.
A sliding window log: keep each client's recent timestamps, drop those older than the window, and allow the request only if fewer than N remain.
JavaScriptclass RateLimiter { constructor(limit, windowMs) { this.limit = limit; this.windowMs = windowMs; this.hits = new Map(); // key -> timestamps inside the window } allow(key, now = Date.now()) { const recent = (this.hits.get(key) ?? []).filter((t) => now - t < this.windowMs); const allowed = recent.length < this.limit; if (allowed) recent.push(now); this.hits.set(key, recent); return allowed; } } const limiter = new RateLimiter(3, 1000); // 3 requests per second per client const times = [0, 100, 200, 300, 1050, 1150]; console.log(times.map((t) => limiter.allow('1.2.3.4', t)).join(' ')); console.log(limiter.allow('5.6.7.8', 300));The fourth request inside one second is refused; by 1050 ms the first has left the window. The clock is a parameter, so tests need no waiting. A token bucket uses O(1) memory per client, and with several instances the counts move to Redis. Reply
429withRetry-After.
Node.js Coding Interview Questions
Small utilities and log processing problems from Node.js rounds; every solution runs here.