JavaScript: the observer pattern with EventEmitter
Let several listeners react to the same event without the emitter knowing about them.
On this page
JavaScript callbacks.
- Let several listeners react to the same event without the emitter knowing about them.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
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.
Code example
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
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
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 |
Behind the scenes
How JavaScript processes this code
A JavaScript engine parses and runs the source. A function body runs when the function is called. console.log writes to the console.
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).