Skip to content
Node.js nd events 4 min read

EventEmitter Basics

Much of Node.js is built around an event-driven model: instead of returning a value, objects announce that something happened and let interested code react. Streams, HTTP servers, child processes, and timers all speak this language. At the heart of it sits the EventEmitter class from the built-in node:events module — a tiny, synchronous publish/subscribe mechanism you can also use in your own code to keep components loosely coupled. Once you understand emit, on, and once, the rest of Node’s async surface feels far more familiar.

Creating an EventEmitter

The events module exports the EventEmitter class. You create an instance, register listeners for named events with .on(), and trigger those listeners by calling .emit() with the same event name. Event names are arbitrary strings (symbols are also allowed), and a single emitter can carry as many distinct events as you like.

import { EventEmitter } from 'node:events';

const bus = new EventEmitter();

bus.on('greet', () => {
  console.log('Hello from the event bus!');
});

bus.emit('greet');

Output:

Hello from the event bus!

In CommonJS the import looks slightly different, but the API is identical:

const { EventEmitter } = require('node:events');

A key detail: listeners run synchronously in the order they were registered. When emit() returns, every listener for that event has already finished. This makes reasoning about ordering simple, but it also means a slow listener will block whatever called emit().

Listening with .on() and firing with .emit()

.on(eventName, listener) adds a listener that fires every time the event is emitted. The same emitter can have multiple listeners for one event, and they all run on each emit().

import { EventEmitter } from 'node:events';

const orders = new EventEmitter();

orders.on('placed', () => console.log('Logging the order...'));
orders.on('placed', () => console.log('Sending confirmation email...'));

orders.emit('placed');
orders.emit('placed');

Output:

Logging the order...
Sending confirmation email...
Logging the order...
Sending confirmation email...

.emit() returns true if the event had at least one listener and false otherwise — useful for detecting whether anyone is actually listening.

Passing arguments to listeners

Any arguments you pass to .emit() after the event name are forwarded, in order, to every listener. This is how you carry a payload — an object, an id, an error, whatever the event needs to describe what happened.

import { EventEmitter } from 'node:events';

const orders = new EventEmitter();

orders.on('placed', (order, customer) => {
  console.log(`Order #${order.id} for ${customer} — total $${order.total}`);
});

orders.emit('placed', { id: 1042, total: 59.99 }, 'Ada Lovelace');

Output:

Order #1042 for Ada Lovelace — total $59.99

Prefer a single payload object over many positional arguments when an event carries more than one or two values. It keeps listener signatures stable as the event evolves, since adding a field doesn’t shift argument positions.

Note that arrow functions don’t bind this. If you use a regular function listener, this refers to the emitter instance — handy for inspecting the emitter, but a common source of confusion when refactoring between the two styles.

One-time listeners with .once()

Sometimes you only care about the first occurrence of an event — a ready signal, a one-shot connected notification, or the result of a single async operation. .once() registers a listener that runs at most once and then automatically removes itself.

import { EventEmitter } from 'node:events';

const db = new EventEmitter();

db.once('ready', () => console.log('Database connected — running migrations'));

db.emit('ready'); // fires the listener
db.emit('ready'); // listener already removed, nothing happens

Output:

Database connected — running migrations

The table below summarizes the core methods you’ll reach for most often.

MethodFiresAuto-removesTypical use
.on(name, fn)Every emitNoRecurring events (data chunks, requests)
.once(name, fn)First emit onlyYesOne-shot signals (ready, connected)
.emit(name, ...args)Trigger listeners with a payload
.off(name, fn)Remove a specific listener

A practical example

Here’s a small task runner that broadcasts progress as it works — a realistic pattern for decoupling doing work from reporting on it.

import { EventEmitter } from 'node:events';
import { setTimeout as sleep } from 'node:timers/promises';

class TaskRunner extends EventEmitter {
  async run(tasks) {
    this.emit('start', tasks.length);
    for (const [index, task] of tasks.entries()) {
      await sleep(50); // simulate work
      this.emit('progress', { done: index + 1, total: tasks.length, task });
    }
    this.emit('done');
  }
}

const runner = new TaskRunner();

runner.once('start', (total) => console.log(`Running ${total} tasks`));
runner.on('progress', ({ done, total, task }) =>
  console.log(`[${done}/${total}] finished "${task}"`),
);
runner.once('done', () => console.log('All tasks complete'));

await runner.run(['build', 'test', 'deploy']);

Output:

Running 3 tasks
[1/3] finished "build"
[2/3] finished "test"
[3/3] finished "deploy"
All tasks complete

Extending EventEmitter like this gives your own classes the full event API for free, which is exactly how Node’s own http.Server and stream classes are built.

Best Practices

  • Import from node:events with the node: prefix so the source is unambiguous and resolution is faster.
  • Name events as lowercase verbs or past-tense facts (data, placed, closed) for consistency with Node’s built-in emitters.
  • Pass a single structured payload object instead of many positional arguments so listener signatures stay stable.
  • Use .once() for signals that logically happen once — it prevents leaks and stale handlers without manual cleanup.
  • Keep listeners fast and non-blocking; because they run synchronously, a slow listener stalls the emit() caller and everything after it.
  • Always attach an error listener to long-lived emitters — an emitted error with no listener throws and can crash the process.
Last updated June 14, 2026
Was this helpful?