Skip to content
JavaScript Basics: Connect Listeners with EventEmitter

JavaScript Basics: Connect Listeners with EventEmitter

October 17, 2026

Node.js EventEmitter lets one part of a program announce an event and another react to it. These examples keep the listeners synchronous so their order is visible.

These examples use built-in features and were tested on Linux with Node.js v24.19.0. No packages are needed. Assertions check the results before the program finishes.

Prepare a scratch directory

You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. These commands stop the shell if setup fails. Save each named source file as UTF-8; .mjs selects ES modules.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Observe synchronous listener order

Save this as event-order.mjs:

import assert from 'node:assert/strict';

import { EventEmitter } from 'node:events';
const events = new EventEmitter();
const order = [];
events.on('ready', name => order.push(`first:${name}`));
events.on('ready', name => order.push(`second:${name}`));
order.push('before');
const hadListeners = events.emit('ready', 'report');
order.push('after');
assert.equal(hadListeners, true);
assert.deepEqual(order, ['before', 'first:report', 'second:report', 'after']);
console.log(order.join(' -> '));

Run it:

node event-order.mjs

Output:

before -> first:report -> second:report -> after

emit() calls listeners synchronously in registration order. Its boolean return value indicates whether the event had listeners, not whether their work succeeded. Returned listener values are ignored; ordinary emitters do not await async listeners.

2. Run once and detach a listener

Save this as event-lifetime.mjs:

import assert from 'node:assert/strict';

import { EventEmitter } from 'node:events';
const events = new EventEmitter();
let notifications = 0;
let firstNotifications = 0;
function countNotification() { notifications += 1; }
events.on('update', countNotification);
events.once('update', () => { firstNotifications += 1; });
events.emit('update');
events.emit('update');
events.off('update', countNotification);
const hadListeners = events.emit('update');
assert.equal(notifications, 2);
assert.equal(firstNotifications, 1);
assert.equal(hadListeners, false);
console.log(`Regular listener calls: ${notifications}`);
console.log(`One-time listener calls: ${firstNotifications}`);
console.log(`Listeners after removal: ${events.listenerCount('update')}`);

Run it:

node event-lifetime.mjs

Output:

Regular listener calls: 2
One-time listener calls: 1
Listeners after removal: 0

once() removes its listener before invoking it. off() needs the same function reference used when registering. Keep that reference when you need to unsubscribe. Registering the same function repeatedly creates multiple registrations; one removal removes at most one.

3. Handle the special error event

Save this as event-errors.mjs:

import assert from 'node:assert/strict';

import { EventEmitter } from 'node:events';
const events = new EventEmitter();
const messages = [];
events.on('error', error => messages.push(error.message));
events.emit('error', new Error('Input unavailable'));
assert.deepEqual(messages, ['Input unavailable']);
console.log(`Handled: ${messages[0]}`);

const unhandledEvents = new EventEmitter();
assert.throws(
  () => unhandledEvents.emit('error', new Error('No error listener')),
  { message: 'No error listener' },
);
console.log('Missing error listener: emit throws');

Run it:

node event-errors.mjs

Output:

Handled: Input unavailable
Missing error listener: emit throws

The error event is special: without an error listener, emitting it throws. An uncaught exception normally terminates the process. A listener that itself throws still propagates that exception; registering an error listener is not a general exception shield.

Clean up

In the same terminal, remove only the example files, return to your original directory, and remove the empty scratch directory:

rm -- event-order.mjs event-lifetime.mjs event-errors.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on