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
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' listeneron(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
emitreturns. - 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)(alsoremoveListener) 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:
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:
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:
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.emitreturnstrueif the event had listeners andfalseif nobody was listening, which helps when debugging a missing reaction.- The
onfunction fromnode: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 anAbortControllerto stop it; the loop then throws anAbortError.
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.
A shop that announces orders
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')}`);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: 2Line-by-line explanation
- Lines 5 to 10Two listeners react to the same event, each doing its own job.
- Line 11once runs only for the first order, then removes itself.
- Line 15emit passes the order object to every listener, in the order they were added.
- Line 17The once listener is gone, so two remain.
A custom emitter class with errors
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');photo.jpg: 50%
photo.jpg: 100%
photo.jpg uploaded
Upload failed: video.mp4 is too large (250 MB)
Program still runningLine-by-line explanation
- Line 3Extending EventEmitter gives the class on, emit and the rest.
- Line 6Errors are reported through the special error event.
- Line 10Events can carry several arguments.
- Line 19Because an error listener exists, the failure is handled instead of crashing the program.
Removing listeners and awaiting an event
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`);tick 1
tick 2
all done: handled 2 ticksLine-by-line explanation
- Line 6A named function can be removed later; an inline arrow function couldn't.
- Line 9The listener removes itself after the second tick, so ticks 3 and 4 are ignored.
- Lines 13 to 16The events are emitted later, from a timer.
- Line 18once returns a promise of the event's arguments, as an array.
Common Node.js Basics Mistakes
No listener for error
const job = new EventEmitter();
job.emit('error', new Error('Disk full')); // throws and crashes the programconst job = new EventEmitter();
job.on('error', (err) => console.error(err.message));
job.emit('error', new Error('Disk full'));Removing an inline listener
emitter.on('data', (d) => console.log(d));
emitter.off('data', (d) => console.log(d)); // a different function, so nothing is removedconst log = (d) => console.log(d);
emitter.on('data', log);
emitter.off('data', log);Adding listeners inside a request handler
app.get('/status', (req, res) => {
queue.on('job', updateStats); // one more listener on every request
res.send('ok');
});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.
Complete the line to trigger the 'saved' event.
store.('saved', { id: 7 });
Which module provides EventEmitter?
What does this print?
const e = new EventEmitter();
e.once('hi', () => console.log('hello'));
e.emit('hi');
e.emit('hi');
What happens if 'error' is emitted with no error listener?
Are EventEmitter listeners called synchronously or asynchronously?
Node.js Basics Quiz
5 questions to check what you learned.
Which method adds a listener that runs every time an event is emitted?
- once
- emit
- on
- off
How do you remove a listener?
- emit('remove')
- delete emitter[name]
- off(name, sameFunction)
- on(name, null)
What does await once(emitter, 'ready') give you?
- The emitter
- true
- The listener count
- An array of the arguments passed to the event
What does a MaxListenersExceededWarning usually point to?
- Listeners being added repeatedly, which leaks memory
- A slow listener
- An error event
- Too many emitters
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