JavaScript Basics: Connect Listeners with EventEmitter
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 11. 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.mjsOutput:
before -> first:report -> second:report -> afteremit() 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.mjsOutput:
Regular listener calls: 2
One-time listener calls: 1
Listeners after removal: 0once() 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.mjsOutput:
Handled: Input unavailable
Missing error listener: emit throwsThe 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_dirAcknowledgements
This post was created with assistance from AI.