2.2 · EventEmitter & event-driven programming

Level 2 · Node.js Core

2.2EventEmitter & event-driven programming

Goal: Understand and use Node's event-driven model (emitting/listening via EventEmitter, .on/.emit), central to working with Node APIs.
Estimated time: 10 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “Callbacks, Promises and EventEmitter are all the same — I'll use Promises for everything”. No. A Promise represents ONE single result and resolves ONCE: perfect for “read this file and give me its content”. An EventEmitter publishes events that can happen MANY times (each server request, each stream chunk) to ZERO, ONE or MANY independent listeners. Wanting one Promise per received request or per data chunk means using the wrong tool: repeated flow plus decoupling = EventEmitter.
Emit, listen, react Decoupled by name Emitter.emit(name, data): something happened+Event nameThe only link between the two sides+Listeners.on(name, reaction): each reacts its own way Adding behaviour = adding a listener, without touching the emitter.
Decoupled by name
Text version of the diagram (SEO / screen readers / translation): 1) Salim's import EMITS: one event per processed file, then a completion event — it knows no recipient. 2) The EVENT NAME links them: the display and the log subscribe with .on, independently. 3) Each REACTS its own way: progress bar on one side, log line on the other. Lesson: emit/listen decouples — and this is the model of native servers, streams and sockets.

🔎 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

  1. Create an EventEmitter (const emitter = new EventEmitter()), register a listener with .on('event', callback) and emit with .emit('event', data).
  2. Pass data when emitting and receive it in the listener; add several listeners on the same event.
  3. Try .once() (single listen) and always listen for the 'error' event (otherwise a crash).
  4. Notice that native Node objects are EventEmitters: listen for example to an HTTP server's or a stream's events (.on('data', ...)).
You understand and use Node's event model (emitting/listening via EventEmitter, .on/.emit), central to working with Node APIs (servers, streams…).

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.

Section exit checklist
  • 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.
Self-rating 1-5: do I decouple with events when needed?do I protect each emitter with an error listener?

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.

Check your understanding

Salim wants to react to each imported file, as many times as needed, with two independent displays. Which tool?

Additional resources (optional, to go further)

Level 2 examFresh, harder questions to test what you've learned