Promise.all Polyfill
Implement your own Promise.all polyfill to understand how concurrent Promise handling works under the hood.
JavaScript · Promises · Async/Await — Pratik Rai ·
Implement a pub/sub EventEmitter class with on, off, emit, and once — the pattern behind Node's events module.
An event emitter is the publish/subscribe pattern in about forty lines: objects register callbacks against named events, and something else fires those events without knowing who is listening. It underpins Node's EventEmitter, the DOM's addEventListener, and most state management libraries. Building one is a good exercise because the naive version has a memory leak and a re-entrancy bug that are easy to demonstrate.
JSfile.javascript1const bus = new EventEmitter(); 2 3const off = bus.on('user:login', (user) => console.log(user.name)); 4bus.once('ready', () => console.log('ready, once only')); 5 6bus.emit('user:login', { name: 'Ada' }); 7off(); // unsubscribe
Four methods carry the pattern: on to subscribe, off to unsubscribe, once for a self-removing listener, and emit to fire.
JSfile.javascript1class EventEmitter { 2 #listeners = new Map(); 3 4 on(event, handler) { 5 if (typeof handler !== 'function') { 6 throw new TypeError('handler must be a function'); 7 } 8 if (!this.#listeners.has(event)) { 9 this.#listeners.set(event, new Set()); 10 } 11 this.#listeners.get(event).add(handler); 12 13 // Return an unsubscribe function — far less error-prone than off(). 14 return () => this.off(event, handler); 15 } 16 17 off(event, handler) { 18 const handlers = this.#listeners.get(event); 19 if (!handlers) return this; 20 handlers.delete(handler); 21 if (handlers.size === 0) this.#listeners.delete(event); 22 return this; 23 } 24 25 once(event, handler) { 26 const wrapper = (...args) => { 27 this.off(event, wrapper); 28 handler.apply(this, args); 29 }; 30 wrapper.listener = handler; // so off(event, handler) can find it 31 return this.on(event, wrapper); 32 } 33 34 emit(event, ...args) { 35 const handlers = this.#listeners.get(event); 36 if (!handlers || handlers.size === 0) return false; 37 38 // Copy before iterating: handlers may subscribe or unsubscribe during emit. 39 for (const handler of [...handlers]) { 40 try { 41 handler.apply(this, args); 42 } catch (error) { 43 // One bad listener must not stop the others. 44 queueMicrotask(() => { throw error; }); 45 } 46 } 47 return true; 48 } 49}
Map keyed by event name, Set of handlers per event. The Set gives O(1) add and delete and naturally prevents the same function being registered twice — though note that Node's EventEmitter does allow duplicates, so this is a deliberate divergence.
Copy the handler set before iterating. A listener that calls off during emit mutates the collection you are looping over. With a Set, deleting during iteration can skip the next entry. Spreading into an array first makes the emit operate on a stable snapshot — which is also what the DOM does.
Unsubscribing needs the same reference. off(event, fn) compares by identity, so an inline arrow can never be removed:
JSfile.javascript1bus.on('tick', () => update()); // unremovable 2bus.off('tick', () => update()); // different function — does nothing
This is why on returns an unsubscribe closure. It removes the whole category of bug.
A throwing listener must not break the chain. Without the try/catch, the third listener never runs because the second threw. Rethrowing inside queueMicrotask preserves the error for global handlers and error reporting instead of swallowing it, while keeping the emit loop going.
once wrapper problemonce registers a wrapper, not your function. So off(event, originalHandler) cannot find anything to remove — the set contains the wrapper. Storing wrapper.listener = handler lets off search by that property:
JSfile.javascript1off(event, handler) { 2 const handlers = this.#listeners.get(event); 3 if (!handlers) return this; 4 for (const h of handlers) { 5 if (h === handler || h.listener === handler) handlers.delete(h); 6 } 7 if (handlers.size === 0) this.#listeners.delete(event); 8 return this; 9}
Node solves it the same way, and it is a detail interviewers like precisely because it only shows up when you test once plus off together.
This is the pattern's defining hazard. An emitter holds a strong reference to every handler, and a handler closes over its surrounding scope. Subscribe from a component and never unsubscribe, and the component — plus everything it references — can never be collected.
Node prints a warning past ten listeners on one event for exactly this reason: it is usually a leak, not a legitimate need.
Every subscription needs a matching teardown:
JSfile.javascript1useEffect(() => bus.on('user:login', handleLogin), []);
Returning the unsubscribe function directly works because on returns one — the effect cleanup runs it on unmount.
Deleting the event key once its set is empty matters too. Without it, an app that subscribes and unsubscribes across thousands of event names accumulates empty Set objects forever.
"Add wildcard events." Support user:* matching user:login. Requires pattern matching in emit and a decision about whether wildcard handlers fire before or after exact matches.
"Make emit async." Await each handler in sequence, or run them concurrently with Promise.all. The interesting question is what happens when one rejects.
"How does this differ from the DOM?" addEventListener adds capture/bubble phases, preventDefault, and an options object. The core registry is the same idea.
Map of event name to Set of handlers gives O(1) subscribe and unsubscribe.on — identity-based removal is too easy to get wrong.once must store a back-reference so off can find the original handler.Goal: Implement all four EventEmitter methods. Pay attention to the .slice() in emit and the wrapper in once.
Continue learning with these related challenges
Implement your own Promise.all polyfill to understand how concurrent Promise handling works under the hood.
JavaScript · Promises · Async/Await — Pratik Rai ·
Implement a Function.prototype.call polyfill from scratch. Master JavaScript this binding, execution context, primitive boxing, and safe property assignment techniques.
JavaScript · Functions · this — Pratik Rai ·
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement your own Promise.all polyfill to understand how concurrent Promise handling works under the hood.
JavaScript · Promises · Async/Await
Pratik Rai ·
Implement a Function.prototype.call polyfill from scratch. Master JavaScript this binding, execution context, primitive boxing, and safe property assignment techniques.
JavaScript · Functions · this
Pratik Rai ·
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills
Pratik Rai ·