2.2EventEmitter & event-driven programming
The essentials in 30 seconds
- Emit then listen: .on (registration) declares the reaction, .emit (trigger) calls it — the two sides never know each other.
- Everywhere under the hood: HTTP servers, streams, sockets are EventEmitters — knowing how to listen means knowing how to use Node.
- error shows no mercy: an error event emitted with no listener crashes the program — always listen for errors.
Ongoing mini-case — Salim reports the import progress
Salim evolves his inventory (2.1): importing the reports takes time, and the team wants to follow the progress without polling his script in a loop. He creates an emitter that publishes one event per processed file plus a completion event — the progress display and the log listen independently, without the import knowing them. In 2.3, the streams themselves will emit data/end events, and in 2.4 the HTTP server will emit a request on each visit.
Sequel to the 2.1 thread: Salim's inventory becomes decoupled through events — and continues in 2.3 and 2.4.
1. Emitting events, listening and reacting
Node is, at its core, event-driven (a reminder from level 0: the event loop). Event-driven programming rests on a simple idea: instead of code calling other functions directly, objects emit events (“something happened”) and other parts of the code listen and react. It is a decoupling mechanism: the emitter doesn't know who listens, the listener doesn't know where the event comes from — they communicate through the event name. Salim's import no longer needs to know the display: it emits, the display listens.
2. .on and .emit, plus once and off
It works with two methods. .on(name, callback): REGISTER a listener — “when X is emitted, run this function”. .emit(name, ...data): EMIT — “trigger X with this data”, all its listeners are then called. You create your emitter with new EventEmitter() from the native events module (or by having a class inherit from it). Also useful: .once() (listen ONCE, automatic unsubscribe), .off() / .removeListener() (stop listening). And the golden rule: the error event is special — emitted with no listener, it crashes the program. So Salim always listens for his import's errors.
3. The pattern behind every Node API
Why it matters: it is the underlying pattern of many APIs — HTTP servers (an event per request), streams ('data', 'end'), sockets, and many libraries expose behaviour through events. Knowing how to listen (.on('data', ...), .on('request', ...)) is a constant in Node. The EventEmitter also enables a decoupled, extensible architecture: adding behaviour = adding a listener, without touching the emitter. You won't necessarily create many custom emitters at first, but you will MEET them constantly — hence the importance of the emit/listen model.
🔎 Concrete example — 🔎 Concrete example — Salim's import progress
One emitter, two independent listeners, as at Salim's:
const { EventEmitter } = require('node:events');
const progression = new EventEmitter();
progression.on('fichier', (nom) => console.log('Processed:', nom));
progression.on('termine', (total) => console.log('Import done:', total));
progression.on('error', (err) => console.error('Import failed:', err.message));
progression.emit('fichier', 'rapport-01.txt');
progression.emit('termine', 1);
Reading: the import emits; the display and the log listen without knowing each other.
Lesson: the error listener is mandatory — otherwise, crash.
Section vocabulary
- Event-driven programming
- Model where objects emit events and other parts of the code react by listening; decoupling through the event name.
Simple example: Salim's import emits; progress and log listen.
What it is not: a direct function call where the caller knows the recipient.
Reflex: several independent reactions = think events.
- EventEmitter
- Native class (events module) at the heart of Node's event model; many native objects (HTTP server, streams) inherit from it.
Simple example: Salim creates new EventEmitter() for his import progress.
What it is not: a Promise, which resolves only once.
Reflex: Node object that emits = probably an EventEmitter.
- .on() / .emit()
- on(event, callback) registers a listener; emit(event, data) triggers the event and calls all its listeners.
Simple example: progression.on('fichier', ...) then progression.emit('fichier', nom).
What it is not: a function return that gives a value back to the caller.
Reflex: .on before .emit, otherwise the event goes nowhere.
- .once() / .off()
- once() listens to an event a single time (auto unsubscribe); off()/removeListener() stops listening.
Simple example: Salim listens for the import-completion event with .once().
What it is not: a listener that stays active forever by default.
Reflex: single expected event = .once().
- error event
- Special event: an emitter that emits error with no listener crashes the program — always listen for errors.
Simple example: Salim adds progression.on('error', ...) from the start.
What it is not: an ordinary event you can safely ignore.
Reflex: new emitter = immediate error listener.
In practice — In practice — Using event-driven programming
- Create an EventEmitter (const emitter = new EventEmitter()), register a listener with .on('event', callback) and emit with .emit('event', data).
- Pass data when emitting and receive it in the listener; add several listeners on the same event.
- Try .once() (single listen) and always listen for the 'error' event (otherwise a crash).
- Notice that native Node objects are EventEmitters: listen for example to an HTTP server's or a stream's events (.on('data', ...)).
Ready-to-say script — explaining decoupling to a colleague (30 s)
“My import knows neither the display nor the log: it just emits one event per processed file. Each subscribes with .on and reacts its own way. If tomorrow we add an e-mail alert, we add a listener — without touching a single line of the import.”
Say it while adding a listener live; the demonstration convinces.
Copy-paste template — event-tracked import
const { EventEmitter } = require('node:events');
class Importeur extends EventEmitter {
async importer(fichiers) {
let traites = 0;
for (const f of fichiers) {
try {
traites += 1;
this.emit('fichier', f, traites, fichiers.length);
} catch (err) {
this.emit('error', err);
return;
}
}
this.emit('termine', traites);
}
}
const importeur = new Importeur();
importeur.on('fichier', (f, n, total) => console.log(n + '/' + total + ' : ' + f));
importeur.once('termine', (total) => console.log('Import done: ' + total + ' files'));
importeur.on('error', (err) => console.error('Failed: ' + err.message));
importeur.importer(['rapport-01.txt', 'rapport-02.txt']);
Solo script
One progress listener is enough: the emitter stays identical, you add listeners as needs grow.
Team in Algiers
Salim adds a listener that logs each file to the shared tracking file — without modifying the import.
One-off event
Import end, initialisation done: .once() avoids double reactions if the event is re-emitted.
Native object
Rather than creating an emitter, listen to the existing one: HTTP server, stream, process — .on('data'/'request'/'exit').
✓ Best practices
- Register listeners (.on) before emitting, otherwise the event goes nowhere.
- Always listen for error on each emitter (otherwise the program crashes).
- Add behaviour by adding a listener, not by modifying the emitter.
✗ Common pitfalls
- Using a Promise for a repeated flow (requests, stream chunks).
- Forgetting the error listener (crash on the first problem).
- Emitting before registering listeners (lost events).
Key takeaways
- EVENT-DRIVEN PROGRAMMING (at Node's core): objects EMIT events, others LISTEN and react — decoupling through the event name.
- The EVENTEMITTER (events module) is the central class; many native objects inherit from it (HTTP server, streams, sockets).
- Two key methods: .on(event, callback) (listen) and .emit(event, data) (trigger). Also: .once() (once), .off() (stop).
- Always listen for the 'error' event (emitted with no listener = crash). Understanding EventEmitter = understanding how to interact with Node APIs.
48h challenge — apply it tomorrow
Turn a loop-based process in your code into an emitter: one event per processed item, a completion event in .once(), and an error listener. Then add a second listener (log, counter) without touching the emitter.
- I can explain emit/listen and name-based decoupling.
- I can use .on/.emit/.once/.off and listen for error.
- I decoupled a real process with two independent listeners.
Frequently asked questions
Why is event-driven programming so present in Node, and where do you meet it?
Because it matches Node's non-blocking async architecture: incoming requests, arriving network data, finishing reads — all happen at unpredictable times, and the natural reaction is “when X happens, do this”. You meet it everywhere: the HTTP server emits on each request, streams emit 'data'/'end'/'error' (section 2-3), sockets signal connection and close, process emits 'exit', and WebSockets (section 4-3) live on events. Like Salim, you spend your time listening — hence the model's importance.
When should I create my own EventEmitter rather than using callbacks or Promises?
When the event can happen several times and decoupling matters. A Promise resolves only once: perfect for “do this operation and give me its result” (reading a file, querying a database). An EventEmitter emits as many times as needed to zero, one or several independent listeners: each server request, each stream chunk, each file in Salim's import. Rule: one single result in a direct relationship = Promise/async-await; repeated flow with decoupled consumers = EventEmitter.
Salim wants to react to each imported file, as many times as needed, with two independent displays. Which tool?