Events & EventEmitter
Reviewed & published by Brayan K
Node's event-driven core is built on the EventEmitter class, which lets one part of your program emit named events and other parts subscribe with listener functions that run whenever those events fire.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll know how to create an emitter, attach and remove listeners, run a listener only once, build your own class that emits events, and handle the special 'error' event so a stray failure never crashes your program.
What You'll Learn in This Lesson
- What EventEmitter is and why Node is event-driven
- Subscribe with .on() and fire events with .emit()
- Pass data from an emitter to its listeners
- Attach multiple listeners and run one with .once()
- Remove listeners with .off() / removeListener()
- Extend EventEmitter and handle the special 'error' event
1️⃣ Creating an Emitter: .on() and .emit()
The pattern has two halves. You subscribe to an event with .on(name, listener), giving it the name of an event and a function to run. Later, you fire that event with .emit(name, ...args), and Node runs every listener subscribed to that name, passing along any extra arguments as data.
Read the worked example below — every line is explained — then run it yourself. Notice how the data you pass to .emit() arrives as the listener's argument.
// EventEmitter is a built-in class. Pull it off the 'node:events' module.
const { EventEmitter } = require('node:events');
// Create an emitter — think of it as a tiny radio station.
const radio = new EventEmitter();
// .on(name, listener) SUBSCRIBES: run this function whenever 'news' fires.
radio.on('news', (headline) => {
console.log(`Listener heard: ${headline}`);
});
// .emit(name, ...args) FIRES the event and passes data to every listener.
radio.emit('news', 'Node 22 released'); // -> Listener heard: Node 22 released
radio.emit('news', 'Events are easy'); // -> Listener heard: Events are easy
// Listeners run SYNCHRONOUSLY, in the order they were registered.2️⃣ Many Listeners, .once(), and Removing Them
One event can have many listeners — they all run, in the order they were added. Three methods give you fine control:
- • .on('e', fn) — subscribe; runs every time 'e' is emitted.
- • .once('e', fn) — runs one time, then removes itself automatically.
- • .off('e', fn) — removes a specific listener (alias: .removeListener('e', fn)). You must pass the same function reference you registered, so name your listener functions when you plan to remove them.
Trace the output below: the .once() listener fires on the first emit only, and .off() stops tick after the first call.
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();
// You can attach MANY listeners to the same event — all of them run.
bus.on('start', () => console.log('A: warming up'));
bus.on('start', () => console.log('B: ready'));
// .once() runs ONE time, then removes itself automatically.
bus.once('start', () => console.log('C: this fires only once'));
// A named function so we can remove it later.
function onTick(n) { console.log(`tick ${n}`); }
bus.on('tick', onTick);
bus.emit('start'); // A, B, and C all run
bus.emit('start'); // A and B run again — C is already gone
bus.emit('tick', 1); // -> tick 1
bus.off('tick', onTick); // .off() removes the listener (alias: removeListener)
bus.emit('tick', 2); // nothing — no listener left3️⃣ Your Own Emitter Class & the 'error' Event
Most real Node objects that emit events — servers, streams, sockets — are classes that extend EventEmitter. You can do the same: class Logger extends EventEmitter gives your class .on(), .emit(), and all the rest, so it can announce what it's doing.
One event name is special: 'error'. If an emitter emits 'error' and no listener is registered for it, Node throws and the process exits. Always attach an .on('error', ...) handler to any emitter that might fail.
const { EventEmitter } = require('node:events');
// Your own classes can BE emitters by extending EventEmitter.
class Logger extends EventEmitter {
write(message) {
// Do the work, then announce it by emitting a custom event.
this.emit('log', { message, time: Date.now() });
}
}
const logger = new Logger();
// Subscribe to the custom 'log' event this class emits.
logger.on('log', (entry) => {
console.log(`[LOG] ${entry.message}`);
});
logger.write('user signed in'); // -> [LOG] user signed in
// 'error' is SPECIAL: if you emit it with no listener, the process CRASHES.
logger.on('error', (err) => {
console.log(`Handled: ${err.message}`);
});
logger.emit('error', new Error('disk full')); // -> Handled: disk fullYour turn. The program below works once you fill in the three blanks marked ___. Follow the 👉 hints, then run it and compare with the expected output.
const { EventEmitter } = require('node:events');
// 🎯 YOUR TURN — replace each ___ then run it.
// 1) Create a new emitter
const emitter = new ___(); // 👉 the class you imported above
// 2) Register a listener for the 'greet' event
emitter.___('greet', (name) => { // 👉 the method that SUBSCRIBES to an event
console.log(`Hello, ${name}!`);
});
// 3) Fire the 'greet' event and pass a name along
emitter.___('greet', 'Sam'); // 👉 the method that FIRES the event
// ✅ Expected output:
// Hello, Sam!Mini-Challenge: A Tiny Order Tracker
No blanks this time — just a brief and an outline to keep you on track. Write it yourself, run it, and check your output against the example in the comments. This is exactly the kind of small program that builds real fluency with events.
const { EventEmitter } = require('node:events');
// 🎯 MINI-CHALLENGE: a tiny order tracker
// 1. Create an EventEmitter called 'shop'.
// 2. Register a listener for the 'order' event that logs
// "New order: <item>" using the data you pass in.
// 3. Use .once() for a 'open' event that logs "Shop is open!".
// 4. Emit 'open' twice, then emit 'order' with 'Coffee'.
//
// ✅ Example output:
// Shop is open!
// New order: Coffee
// your code hereCommon Errors (and the fix)
- Emitting before listeners are registered — Order matters. .emit() only calls listeners that already exist. If you call .emit('ready') before the matching .on('ready', ...), the listener silently never runs. Register listeners first.
- An unhandled 'error' event crashes the process — Emitting 'error' with no .on('error', ...) listener throws and exits Node. Always handle 'error' on emitters that can fail.
- MaxListenersExceededWarning — Adding more than 10 listeners to one event triggers a memory-leak warning. Usually it means you're adding listeners in a loop and never removing them. Clean up with .off(), or raise the cap with emitter.setMaxListeners(n) if you truly need more.
- Forgetting to .emit() — Registering a listener does nothing on its own. The event has to actually be fired. If your listener never runs, check that something calls .emit() with the exact same event name (it's case-sensitive).
- Arrow vs regular functions and this — In a regular function listener, this refers to the emitter. An arrow function keeps this from the surrounding scope instead. Use a regular function when you need this to be the emitter.
Pro Tips
- 💡 Name your listeners when you plan to remove them — .off() needs the exact same function reference, which an anonymous arrow can't give you.
- 💡 Always handle 'error' on long-lived emitters; one unhandled error event takes down the whole process.
- 💡 Use .listenerCount('e') to debug leaks and confirm listeners were actually added or removed.
📋 Quick Reference — EventEmitter
| Concept | Node Syntax |
|---|---|
| Create an emitter | new EventEmitter() |
| Listen for an event | emitter.on('e', fn) |
| Listen one time only | emitter.once('e', fn) |
| Fire an event | emitter.emit('e', data) |
| Remove a listener | emitter.off('e', fn) |
| Count listeners | emitter.listenerCount('e') |
🎉 Lesson Complete!
- ✅ EventEmitter powers Node's event-driven core
- ✅ .on() subscribes; .emit() fires and passes data
- ✅ .once() runs a listener a single time
- ✅ .off() / removeListener() detach listeners
- ✅ Classes can extend EventEmitter to emit their own events
- ✅ An unhandled 'error' event crashes the process — always handle it
Practice quiz
Which module provides the EventEmitter class?
- node:emitter
- node:events
- node:stream
- node:bus
Answer: node:events. EventEmitter is pulled from the built-in node:events module.
What does emitter.on('news', fn) do?
- Fires the 'news' event
- Removes a listener for 'news'
- Subscribes fn to run whenever 'news' is emitted
- Runs fn exactly once
Answer: Subscribes fn to run whenever 'news' is emitted. .on() subscribes a listener that runs every time the named event is emitted.
Which method fires an event and passes data to every listener?
- .emit(name, ...args)
- .on(name, fn)
- .once(name, fn)
- .off(name, fn)
Answer: .emit(name, ...args). .emit(name, ...args) fires the event and passes the extra arguments to each listener.
How does .once() differ from .on()?
- It never runs the listener
- It runs the listener on every emit, twice as fast
- It only works for the 'error' event
- It runs the listener one time, then removes it automatically
Answer: It runs the listener one time, then removes it automatically. .once() runs its listener the first time the event fires and then removes itself.
Are EventEmitter listeners called synchronously or asynchronously?
- Synchronously, in registration order, before emit returns
- Asynchronously on the next tick
- In random order
- Only after a setTimeout
Answer: Synchronously, in registration order, before emit returns. emit calls every listener right away, in the order they were registered, before it returns.
What happens if you emit 'error' with no listener registered for it?
- Nothing happens
- Node throws and the process exits
- It logs a warning and continues
- It retries the emit
Answer: Node throws and the process exits. The 'error' event is special: emitting it with no listener throws and crashes the process.
What does MaxListenersExceededWarning usually indicate?
- The emitter is too fast
- An event name is misspelled
- Listeners are being added in a loop and never removed
- The process is out of memory
Answer: Listeners are being added in a loop and never removed. By default more than 10 listeners on one event warns of a likely leak from listeners never removed.
To remove a listener with .off(), what must you pass?
- The event name only
- Any function with the same body
- A new arrow function
- The exact same function reference you registered
Answer: The exact same function reference you registered. .off() needs the same function reference, which is why you name listeners you plan to remove.
How do you give your own class .on() and .emit()?
- class Logger extends EventEmitter
- class Logger implements EventEmitter
- new EventEmitter(Logger)
- EventEmitter.call(Logger)
Answer: class Logger extends EventEmitter. Extending EventEmitter gives your class all the emitter methods so it can announce its own events.
If you call emit('ready') before registering the matching .on('ready', ...), what happens?
- Node buffers the event and runs the listener later
- The listener silently never runs
- It throws an error
- It runs the listener twice
Answer: The listener silently never runs. emit only calls listeners that already exist, so emitting before subscribing means the listener never runs.
Continue this course
- Previous: Paths & OS
- Next: Streams & Buffers — Process large data efficiently with readable and writable streams
- Quick reference: Node.js cheat sheet › Common Patterns
Frequently asked questions
What is the difference between .on() and .once()?
Both subscribe a listener to an event. .on() keeps the listener attached, so it runs every single time the event is emitted. .once() runs the listener the first time the event fires and then automatically removes it, so it never runs again. Use .once() for one-shot things like a 'ready' or 'connected' signal that only happens a single time.
Why does emitting an 'error' event sometimes crash my Node program?
The 'error' event is treated specially by EventEmitter. If you emit 'error' and there is no listener registered for it, Node throws the error and, because nothing handled it, the process exits. The fix is to always attach an emitter.on('error', handler) listener to any emitter that might emit errors, so the error is handled gracefully instead of taking down your program.
What does MaxListenersExceededWarning mean?
By default each EventEmitter warns if you add more than 10 listeners for the same event, because that often signals a leak where listeners are being added in a loop and never removed. It is only a warning, not an error. If you genuinely need more, call emitter.setMaxListeners(n) to raise the limit, but first check that you are not forgetting to call .off() to clean up old listeners.
Are EventEmitter listeners synchronous or asynchronous?
Listeners run synchronously. When you call emitter.emit('event'), Node calls every listener for that event right away, one after another, in the order they were registered, before emit returns. If a listener needs to do async work, it can start it (for example with a promise or setImmediate), but emit itself does not wait for that work to finish.