JavaScript: the observer pattern with EventEmitter
Let several listeners react to the same event without the emitter knowing about them.
const { EventEmitter } = require("node:events");
const bus = new EventEmitter();
const log = [];
bus.on("order", (id) => log.push("logged " + id));
bus.on("order", (id) => console.log("notify customer for order " + id));
bus.once("order", (id) => console.log("first order bonus for " + id));
bus.emit("order", 1);
bus.emit("order", 2);
console.log(log.join(", "));notify customer for order 1 first order bonus for 1 notify customer for order 2 logged 1, logged 2
Explanation
In the observer pattern a subject notifies any number of observers when something happens. The subject only knows that observers are callables, so new behaviour is added by subscribing, with no change to the subject. Node.js implements the pattern in the events module.
on registers a listener that runs for every event, once registers one that runs only for the next event, and emit calls the listeners in the order they were registered. The same idea appears in the browser as addEventListener, and as publish/subscribe in message queues.
Explore the dry run
const { EventEmitter } = require("node:events"); const bus = new EventEmitter();const log = []; bus.on("order", (id) => log.push("logged " + id));bus.on("order", (id) => console.log("notify customer for order " + id));bus.once("order", (id) => console.log("first order bonus for " + id)); bus.emit("order", 1);bus.emit("order", 2);console.log(log.join(", "));Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A listener that records every event. | — | — | |
| A listener that is removed after its first call. | — | — | |
| All three listeners run in the order they were added; only two print. | — | notify customer for order 1 first order bonus for 1 | |
| The once-listener is gone. | — | notify customer for order 1 first order bonus for 1 notify customer for order 2 | |
| The recording listener saw both events. | — | notify customer for order 1 first order bonus for 1 notify customer for order 2 logged 1, logged 2 |
Cost and common mistakes
emit is O(L) for L listeners.
- emit('error') with no error listener throws and can crash the process. Always attach an error handler.
- Listeners run synchronously inside emit. A slow listener delays the emitter and the other listeners.
- Forgetting to remove listeners from long-lived emitters leaks memory; Node warns after 10 listeners for one event.
Try it yourself
How do you remove a listener?
Reveal answer and reasoning
Keep a reference to the function and call bus.off('order', fn), or bus.removeListener('order', fn).