Node.js Events

Use Node's event-driven style with EventEmitter: listen with on and once, emit events with data, remove listeners, handle the special error event, extend EventEmitter in your own classes, and await events with once from node:events.

Lesson 6 of 43Beginner18 min read

Much of Node.js is built around events. An HTTP server emits a request event every time someone visits. A file stream emits data events as chunks arrive and an end event when it's finished. The process object emits exit when the program is about to stop. Your code listens for the events it cares about and runs a function when they happen.

This style is called event-driven programming, and it fits Node's non-blocking design perfectly: instead of waiting for something to happen, you register what to do when it does, and the event loop calls your function at the right moment.

The engine behind it is the EventEmitter class from the built-in node:events module. Any object that extends it can emit named events, and any code can subscribe to them with on. In this lesson you'll create emitters, pass data with events, listen once, remove listeners, handle the special error event correctly, build your own event-emitting class, and wait for an event with await.

Listening and emitting

JavaScript
import { EventEmitter } from 'node:events';

const shop = new EventEmitter();

shop.on('order', (id, total) => {
  console.log(`Order ${id} for £${total}`);
});

shop.emit('order', 1042, 59.9);   // runs every 'order' listener
  • on(name, listener) registers a function to run every time the event is emitted.
  • emit(name, ...args) triggers the event and passes any arguments to the listeners.
  • Listeners run synchronously, in the order they were added, before emit returns.
  • Event names are strings; use clear, consistent names such as 'order', 'order:paid' or 'user-signed-in'.

once, off and listener management

  • once(name, listener) runs the listener only the first time the event is emitted, then removes it automatically.
  • off(name, listener) (also removeListener) removes a specific listener. You need a reference to the same function, so define it with a name rather than inline.
  • listenerCount(name) tells you how many listeners are registered.
  • Node prints a MaxListenersExceededWarning if more than 10 listeners are added for one event. That usually means a listener is being added repeatedly, for example on every request, which is a memory leak.

The error event

'error' is special. If an emitter emits 'error' and no listener is registered for it, Node throws the error, which crashes the program if nothing catches it:

JavaScript
emitter.on('error', (err) => {
  console.error('Something went wrong:', err.message);
});

Always add an 'error' listener to emitters that can fail, such as streams, servers and sockets.

Your own emitter class and awaiting events

Extend EventEmitter to give a class its own events:

JavaScript
class Download extends EventEmitter {
  start() {
    this.emit('progress', 50);
    this.emit('done', 'file.zip');
  }
}

The once function from node:events returns a promise that resolves the next time an event fires, so you can await it: const [file] = await once(download, 'done');. It rejects if an 'error' event happens first.

Async listeners, listener order and event streams

emit doesn't wait for listeners and ignores what they return. If a listener is an async function and it throws, the error becomes a rejected promise that emit never sees, so even an 'error' listener won't hear about it. Create the emitter with captureRejections: true and Node routes those rejections to the 'error' event for you:

JavaScript
const store = new EventEmitter({ captureRejections: true });
store.on('save', async () => { throw new Error('disk full'); });
store.on('error', (err) => console.log('caught:', err.message));
store.emit('save');   // later prints "caught: disk full"

A few other tools help with ordering and flow:

  • prependListener(name, fn) adds a listener to the front of the queue, so it runs before ones added earlier.
  • emit returns true if the event had listeners and false if nobody was listening, which helps when debugging a missing reaction.
  • The on function from node:events (not the method) turns an event into an async iterator: for await (const [n] of on(ticker, 'tick')) handles each event in turn. The loop never finishes by itself, so pass { signal } from an AbortController to stop it; the loop then throws an AbortError.

Node.js Basics Syntax

EventEmitter API

import { EventEmitter, once } from 'node:events';
const e = new EventEmitter();
e.on('tick', (n) => {});
e.once('ready', () => {});
e.emit('tick', 1);
function handler() {}
e.on('x', handler);   e.off('x', handler);
e.on('error', (err) => {});
e.listenerCount('tick');
class Job extends EventEmitter { run() { this.emit('done'); } }
const [value] = await once(e, 'ready');

Node.js Basics Summary

  • EventEmitter from node:events lets objects emit named events.
  • on adds a listener; emit triggers listeners synchronously, in order, with arguments.
  • once listens a single time; off removes a named listener.
  • An unhandled 'error' event throws, so always listen for errors.
  • Extend EventEmitter for your own classes; await once(emitter, name) waits for an event.

Node.js Basics Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A shop that announces orders

Node.js
import { EventEmitter } from 'node:events';

const shop = new EventEmitter();

shop.on('order', (order) => {
  console.log(`Email: thanks for order #${order.id}`);
});
shop.on('order', (order) => {
  console.log(`Stock: reserve ${order.items.length} items`);
});
shop.once('order', () => {
  console.log('First order of the day!');
});

shop.emit('order', { id: 101, items: ['mug', 'lamp'] });
shop.emit('order', { id: 102, items: ['pen'] });
console.log(`Listeners left: ${shop.listenerCount('order')}`);
Output
Email: thanks for order #101
Stock: reserve 2 items
First order of the day!
Email: thanks for order #102
Stock: reserve 1 items
Listeners left: 2
Line-by-line explanation
  1. Lines 5 to 10Two listeners react to the same event, each doing its own job.
  2. Line 11once runs only for the first order, then removes itself.
  3. Line 15emit passes the order object to every listener, in the order they were added.
  4. Line 17The once listener is gone, so two remain.
Example 2

A custom emitter class with errors

Node.js
import { EventEmitter } from 'node:events';

class Uploader extends EventEmitter {
  upload(name, sizeMb) {
    if (sizeMb > 10) {
      this.emit('error', new Error(`${name} is too large (${sizeMb} MB)`));
      return;
    }
    for (let p = 50; p <= 100; p += 50) {
      this.emit('progress', name, p);
    }
    this.emit('done', name);
  }
}

const up = new Uploader();
up.on('progress', (name, pct) => console.log(`${name}: ${pct}%`));
up.on('done', (name) => console.log(`${name} uploaded`));
up.on('error', (err) => console.log(`Upload failed: ${err.message}`));

up.upload('photo.jpg', 2);
up.upload('video.mp4', 250);
console.log('Program still running');
Output
photo.jpg: 50%
photo.jpg: 100%
photo.jpg uploaded
Upload failed: video.mp4 is too large (250 MB)
Program still running
Line-by-line explanation
  1. Line 3Extending EventEmitter gives the class on, emit and the rest.
  2. Line 6Errors are reported through the special error event.
  3. Line 10Events can carry several arguments.
  4. Line 19Because an error listener exists, the failure is handled instead of crashing the program.
Example 3

Removing listeners and awaiting an event

Node.js
import { EventEmitter, once } from 'node:events';

const timer = new EventEmitter();
let ticks = 0;

function onTick(n) {
  ticks++;
  console.log(`tick ${n}`);
  if (n === 2) timer.off('tick', onTick);
}
timer.on('tick', onTick);

setTimeout(() => {
  for (let n = 1; n <= 4; n++) timer.emit('tick', n);
  timer.emit('finished', ticks, 'all done');
}, 10);

const [count, message] = await once(timer, 'finished');
console.log(`${message}: handled ${count} ticks`);
Output
tick 1
tick 2
all done: handled 2 ticks
Line-by-line explanation
  1. Line 6A named function can be removed later; an inline arrow function couldn't.
  2. Line 9The listener removes itself after the second tick, so ticks 3 and 4 are ignored.
  3. Lines 13 to 16The events are emitted later, from a timer.
  4. Line 18once returns a promise of the event's arguments, as an array.

Common Node.js Basics Mistakes

No listener for error

Don't
const job = new EventEmitter();
job.emit('error', new Error('Disk full'));   // throws and crashes the program
Do
const job = new EventEmitter();
job.on('error', (err) => console.error(err.message));
job.emit('error', new Error('Disk full'));

Removing an inline listener

Don't
emitter.on('data', (d) => console.log(d));
emitter.off('data', (d) => console.log(d));   // a different function, so nothing is removed
Do
const log = (d) => console.log(d);
emitter.on('data', log);
emitter.off('data', log);

Adding listeners inside a request handler

Don't
app.get('/status', (req, res) => {
  queue.on('job', updateStats);                // one more listener on every request
  res.send('ok');
});
Do
queue.on('job', updateStats);                  // register once at startup
app.get('/status', (req, res) => res.send('ok'));

Node.js Basics in Real Projects

Events throughout Node.js

http.Server emits request and listening, streams emit data, end and error, and process emits exit, uncaughtException and signals like SIGTERM for graceful shutdown. Libraries such as Socket.IO build real-time messaging on events, and applications use internal event emitters so that placing an order can trigger emails, stock updates and analytics without the order code knowing about any of them.

Node.js Basics FAQ

What is the difference between on and once in Node.js events?

on keeps the listener registered, so it runs every time the event is emitted until you remove it. once runs the listener a single time and then removes it automatically. Use once for events that only matter the first time, such as a server's 'listening' event or a connection's 'close'.

Are EventEmitter listeners asynchronous?

No. When you call emit, every listener runs synchronously, one after another, before emit returns. If a listener does slow work, it holds up the emitter and everything after the emit call. For work that shouldn't block, start it from the listener with an async function or defer it with setImmediate.

How do I fix MaxListenersExceededWarning?

The warning means more than 10 listeners were added for one event on one emitter, which almost always points to a listener being added inside a loop or a request handler without being removed. Find where on is called repeatedly and move it to setup code, or remove it with off when you're done. Only raise the limit with setMaxListeners when you genuinely need many listeners.

When should I use EventEmitter instead of promises or callbacks?

Use an emitter when something happens many times or when several unrelated parts of a program need to react, such as progress updates, incoming messages or an order being placed. Use a promise when an operation completes once with a single result or error. If you need to wait for one event in async code, await once(emitter, 'name') gives you the best of both.

Is Node's EventEmitter the same as addEventListener in the browser?

They solve the same problem with different APIs. Node's EventEmitter uses on and emit with any arguments you like, while the browser's EventTarget uses addEventListener and dispatchEvent with Event objects. Node also includes EventTarget and Event as globals, mainly for web-compatible APIs such as AbortSignal.

Node.js Basics Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line to trigger the 'saved' event.

store.('saved', { id: 7 });
Multiple choiceeasy

Which module provides EventEmitter?

Predict the outputmedium

What does this print?

const e = new EventEmitter();
e.once('hi', () => console.log('hello'));
e.emit('hi');
e.emit('hi');
Multiple choicemedium

What happens if 'error' is emitted with no error listener?

Multiple choicehard

Are EventEmitter listeners called synchronously or asynchronously?

Node.js Basics Quiz

5 questions to check what you learned.

  1. Which method adds a listener that runs every time an event is emitted?

    • once
    • emit
    • on
    • off
  2. How do you remove a listener?

    • emit('remove')
    • delete emitter[name]
    • off(name, sameFunction)
    • on(name, null)
  3. What does await once(emitter, 'ready') give you?

    • The emitter
    • true
    • The listener count
    • An array of the arguments passed to the event
  4. What does a MaxListenersExceededWarning usually point to?

    • Listeners being added repeatedly, which leaks memory
    • A slow listener
    • An error event
    • Too many emitters
  5. Which is a good fit for events rather than a single promise?

    • Reading one file
    • Progress updates that happen many times during an upload
    • A single database query
    • Parsing one JSON string