Promises solved the asynchrony of a single result: you ask for something, you wait, the value or the error arrives, done. But a good deal of what happens in a real system does not have that shape. A session selling out, a sale being recorded, capacity dropping below 10 %, a socket receiving data, a file being modified: that is not a future result, it is a stream of occurrences that happen many times and at unpredictable moments.
Node.js has a mechanism of its own for that, and it is no accessory: it is one of its founding pieces. The events module and its EventEmitter class sit underneath streams, the HTTP server, sockets, child processes and the process object itself. When you write server.on('request', ...) you are using exactly the same API you are about to learn here.
In this lesson you will build Escena Viva's SalesManager: a class that inherits from EventEmitter and notifies the rest of the system when a sale is recorded, when a session's capacity drops below 10 % and — the commitment we made in Module 1 — when a session sells out. Along the way you will discover that listeners run synchronously, why the error event is special to the point of taking the process down, how memory leaks happen through forgotten listeners, and how to await an event with await.
Contents
- The observer pattern and why Node carries it in its DNA
- The
EventEmitterAPI - Listeners run synchronously
- Arguments in
emitand the value ofthis - Inheriting from
EventEmitter: Escena Viva'sSalesManager - The
errorevent: the only one that can kill you - Memory leaks through listeners
events.once(): awaiting an event withawait- Events, callbacks or promises: a decision table
- All of Node is an
EventEmitter
- The observer pattern and why Node carries it in its DNA
The observer pattern solves a concrete design problem: an object needs to notify others that something has happened, without knowing who they are or how many there are.
Without the pattern, the code looks like this:
// WRONG: the sales manager knows all of its consumers.
function recordSale(sessionId, quantity) {
const session = findSession(sessionId);
session.sold += quantity;
if (session.sold >= session.capacity) {
mail.notifyOrganizer(session); // Coupling 1
catalog.markAsSoldOut(session.id); // Coupling 2
metrics.countSoldOut(session.id); // Coupling 3
audit.record('sold-out', session); // Coupling 4
}
}Every new need — "notify the admin panel too", "publish to the cache" — forces you to modify the sales function, which has nothing to do with emails or metrics. It is the recipe for a central module ending up importing half the application.
With the observer pattern:
// RIGHT: the manager only announces. It does not know who is listening.
function recordSale(sessionId, quantity) {
const session = findSession(sessionId);
session.sold += quantity;
if (session.sold >= session.capacity) {
this.emit('session-sold-out', { sessionId, capacity: session.capacity });
}
}And each interested party subscribes on its own:
manager.on('session-sold-out', (data) => mail.notifyOrganizer(data));
manager.on('session-sold-out', (data) => metrics.countSoldOut(data.sessionId));flowchart LR
G["SalesManager<br/><i>emitter</i>"] -->|"emit('session-sold-out')"| E{{"Event"}}
E --> O1["Email notice<br/>to the organizer"]
E --> O2["Update the<br/>public catalog"]
E --> O3["Record a metric"]
E --> O4["Audit"]
style G fill:#2b6cb0,color:#fff
style E fill:#805ad5,color:#fff
The two roles in the pattern:
| Role | Who it is | What it does |
|---|---|---|
| Emitter (subject) | SalesManager |
Announces that something has happened. It does not know the listeners |
| Listener (observer) | Email, metrics, audit… | Subscribes to the events it cares about |
The gain is decoupling: you can add, remove or test listeners without touching a single line of the emitter. In Module 9 this will be decisive, because testing SalesManager will not require any email service.
Node carries this pattern in its DNA for a historical reason: it is the natural way of expressing repeated asynchronous I/O. A socket does not deliver "a result": it delivers data many times, then closes, and along the way it may fail. That is three distinct events, not a promise.
- The
EventEmitter API
EventEmitter APIEventEmitter lives in Node's core events module.
// src/lab/basic-emitter.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// Register a listener
emitter.on('sale-recorded', (data) => {
console.log(`Sale of ${data.quantity} tickets for ${data.sessionId}`);
});
// Emit the event
emitter.emit('sale-recorded', { sessionId: 'ses-002-2', quantity: 3 });
// Sale of 3 tickets for ses-002-2The methods you will use:
| Method | What it does | Returns |
|---|---|---|
on(event, listener) |
Registers a listener. Alias: addListener |
The emitter (chainable) |
once(event, listener) |
Registers a listener that runs only once and is then removed | The emitter |
emit(event, ...args) |
Fires the event, calling every listener in order | true if there were listeners, false if not |
off(event, listener) |
Removes a specific listener. Alias: removeListener |
The emitter |
removeAllListeners([event]) |
Removes every listener of an event (or of all of them) | The emitter |
listenerCount(event) |
How many listeners that event has | A number |
eventNames() |
Names of every event with listeners | An array |
prependListener(event, listener) |
Registers a listener at the front of the list | The emitter |
setMaxListeners(n) |
Changes the leak-warning threshold (10 by default) | The emitter |
on versus once
// src/lab/on-vs-once.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('sale', (n) => console.log(` [on] sale number ${n}`));
emitter.once('sale', (n) => console.log(` [once] sale number ${n}`));
for (let i = 1; i <= 3; i++) {
console.log(`Emitting sale ${i} (listeners: ${emitter.listenerCount('sale')})`);
emitter.emit('sale', i);
}Emitting sale 1 (listeners: 2) [on] sale number 1 [once] sale number 1 Emitting sale 2 (listeners: 1) [on] sale number 2 Emitting sale 3 (listeners: 1) [on] sale number 3
once unregisters itself after the first run. It is the right choice for occurrences that happen only once: 'ready', 'connected', 'closed'.
Unregistering listeners
To be able to remove a listener you need the same function reference you registered it with:
// WRONG: these are two different functions. The off does nothing.
emitter.on('sale', (n) => console.log(n));
emitter.off('sale', (n) => console.log(n));
console.log(emitter.listenerCount('sale')); // 1 <- still there
// RIGHT: we keep the reference.
function onSale(n) {
console.log(n);
}
emitter.on('sale', onSale);
emitter.off('sale', onSale);
console.log(emitter.listenerCount('sale')); // 0It is one of the most common causes of listener memory leaks, and we will come back to it in section 7.
emit tells you whether anyone was listening
Emitting an event with no listeners is not an error: nothing simply happens. With one very important exception, the error event, which we will see in section 6.
- Listeners run synchronously
This is the characteristic of EventEmitter that surprises people most, and the one with real performance consequences:
emitcalls every listener synchronously, one after another, in the order they were registered, and does not return until they have all finished.
EventEmitter is not asynchronous. It is a synchronous dispatch mechanism that happens to be used a lot in asynchronous contexts.
// src/lab/sync-listeners.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('sale', () => console.log(' listener 1 (registered first)'));
emitter.on('sale', () => console.log(' listener 2 (registered second)'));
emitter.on('sale', () => console.log(' listener 3 (registered third)'));
console.log('Before emit');
emitter.emit('sale');
console.log('After emit');Before emit listener 1 (registered first) listener 2 (registered second) listener 3 (registered third) After emit
All three listeners ran before emit handed control back. No event loop, no microtasks.
The performance implication
If emit is synchronous, one slow listener blocks all the others and the main thread. Everything you learned in the architecture lesson applies here:
// src/lab/slow-listener.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('sale', () => console.log(' fast 1'));
emitter.on('sale', () => {
// A listener that does heavy work synchronously.
const deadline = Date.now() + 200;
while (Date.now() < deadline) { /* generating the ticket PDF, for instance */ }
console.log(' SLOW (200 ms)');
});
emitter.on('sale', () => console.log(' fast 2'));
const start = Date.now();
emitter.emit('sale');
console.log(`emit took ${Date.now() - start} ms`);The emit took 201 ms. If that happened inside an Escena Viva HTTP request, the whole server would stop for 200 ms on every sale.
The golden rule for writing listeners:
A listener must be fast. If it has heavy work, it should delegate it.
// WRONG: heavy synchronous work inside the listener.
manager.on('session-sold-out', (data) => {
generateReportPdfSync(data); // Blocks everybody
});
// RIGHT: the listener only queues the work and hands control back.
manager.on('session-sold-out', (data) => {
setImmediate(() => generateReportPdf(data));
});
// BETTER: genuinely asynchronous work, with its own error handling.
manager.on('session-sold-out', (data) => {
emailOrganizer(data).catch((error) => {
console.error(`[mail] failed to notify about ${data.sessionId}: ${error.message}`);
});
});Notice that .catch in the last example. It is mandatory. An async listener whose promise rejects produces an unhandled rejection which, as you learned in the previous lesson, takes the process down. And emit cannot help you: it handed control back long before the promise settled.
Order matters
Since listeners run in registration order, that order is part of the observable behavior:
emitter.on('sale', () => console.log('normal'));
emitter.prependListener('sale', () => console.log('first, always'));
emitter.emit('sale');
// first, always
// normalUsing prependListener to "cut in" at the front is usually a sign of a fragile design. If the order of the listeners genuinely matters, what you probably need is not an event but an explicit sequence of steps.
- Arguments in
emit and the value of this
emit and the value of this4.1 Passing data
emit accepts any number of arguments after the event name, and they all reach every listener:
emitter.emit('sale-recorded', 'ses-002-2', 3, 5400);
emitter.on('sale-recorded', (sessionId, quantity, amountCents) => {
console.log(`${quantity} tickets for ${sessionId} at ${amountCents} cents`);
});It works, but in Escena Viva we will always use a single object:
emitter.emit('sale-recorded', {
sessionId: 'ses-002-2',
eventId: 'evt-002',
quantity: 3,
amountCents: 5400,
availableRemaining: 72,
dateTime: '2026-08-11T18:42:11'
});The reasons are the same as for any API:
| Loose arguments | A single object |
|---|---|
| Order matters and it is easy to get wrong | The names document themselves |
| Adding a field breaks every listener | Adding a field breaks nothing |
| A listener that only wants the third value must declare all three | It destructures only what it needs |
// The listener takes only what it cares about.
manager.on('sale-recorded', ({ sessionId, availableRemaining }) => {
console.log(`${sessionId}: ${availableRemaining} left`);
});4.2 The value of this
Inside a listener registered with function, this is the emitter:
// src/lab/this-in-listeners.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('sale', function () {
console.log('With function, this is:', this.constructor.name);
console.log(' registered events:', this.eventNames());
});
emitter.on('sale', () => {
// An arrow function has NO this of its own: it inherits the one from where it was written.
console.log('With an arrow, this is:', this);
});
emitter.emit('sale');Node binds this to the emitter when the listener is a normal function. With an arrow function, this is the surrounding scope's — in a top-level CommonJS module, module.exports, which shows up as {}.
function () {} |
() => {} |
|
|---|---|---|
this |
The emitter | The one from where it was written |
Access to this.eventNames(), this.off(...) |
Yes | No |
| Inside a class method | this stops being the instance |
this is still the instance |
That last point is the one that decides in practice. Inside a class, the arrow is almost always what you want:
class ControlPanel {
constructor(manager) {
this.soldOut = [];
// With an arrow: this is still the ControlPanel. Correct.
manager.on('session-sold-out', (data) => {
this.soldOut.push(data.sessionId);
});
// With function: this would be the manager, and this.soldOut would be undefined.
// manager.on('session-sold-out', function (data) {
// this.soldOut.push(data.sessionId); // TypeError
// });
}
}Recommendation for the course: use arrow functions in listeners. Accessing this as the emitter is rarely needed, and when you do need it you have the emitter reference in a variable anyway.
- Inheriting from
EventEmitter: Escena Viva's SalesManager
EventEmitter: Escena Viva's SalesManagerWe reach the lesson's central example and the commitment we took on in Module 1. We are going to create a domain class that is an event emitter.
Create src/domain/sales-manager.js:
// src/domain/sales-manager.js
// Records Escena Viva's sales and notifies the rest of the system
// through events, without knowing any of its consumers.
const EventEmitter = require('node:events');
// Threshold below which capacity is considered "low".
const LOW_CAPACITY_THRESHOLD = 0.10; // 10 %
class SalesManager extends EventEmitter {
// Sessions indexed by id, for direct access.
#sessions = new Map();
// Counter of ticket codes issued this year.
#ticketSequence = 0;
constructor(events = []) {
// super() is MANDATORY and must come before using this:
// it initializes EventEmitter's internal machinery.
super();
for (const event of events) {
for (const session of event.sessions) {
this.#sessions.set(session.id, { ...session, eventId: event.id, title: event.title });
}
}
}
get sessionCount() {
return this.#sessions.size;
}
getSession(sessionId) {
return this.#sessions.get(sessionId);
}
// Generates a code in the project's format: EV-2026-000123
#generateTicketCode() {
this.#ticketSequence++;
const year = new Date().getFullYear();
return `EV-${year}-${String(this.#ticketSequence).padStart(6, '0')}`;
}
/**
* Records the sale of "quantity" tickets for a session.
* Emits: 'sale-recorded' always; plus 'low-capacity' or 'session-sold-out'
* if the sale crosses those thresholds.
* Returns the issued tickets.
*/
recordSale(sessionId, quantity, orderId) {
const session = this.#sessions.get(sessionId);
// USAGE errors are thrown: they are the programmer's failures, not occurrences.
if (!session) {
const error = new Error(`Session not found: ${sessionId}`);
error.code = 'SESSION_NOT_FOUND';
throw error;
}
if (!Number.isInteger(quantity) || quantity < 1) {
const error = new Error('The quantity must be a positive integer');
error.code = 'INVALID_QUANTITY';
throw error;
}
const availableBefore = session.capacity - session.sold;
if (quantity > availableBefore) {
const error = new Error(
`Insufficient capacity in ${sessionId}: you asked for ${quantity} and ${availableBefore} are left`
);
error.code = 'INSUFFICIENT_CAPACITY';
throw error;
}
// User JavaScript is single-threaded: this update is atomic.
session.sold += quantity;
const availableAfter = session.capacity - session.sold;
const tickets = [];
for (let i = 0; i < quantity; i++) {
tickets.push({
code: this.#generateTicketCode(),
sessionId,
orderId,
status: 'valid'
});
}
// --- Announcing what happened ---
this.emit('sale-recorded', {
sessionId,
eventId: session.eventId,
title: session.title,
quantity,
amountCents: quantity * session.priceCents,
availableRemaining: availableAfter,
occupancy: Math.round((session.sold / session.capacity) * 100)
});
const availableRatio = availableAfter / session.capacity;
const availableRatioBefore = availableBefore / session.capacity;
if (availableAfter === 0) {
// Sold out: the event we promised in module 1.
this.emit('session-sold-out', {
sessionId,
eventId: session.eventId,
title: session.title,
capacity: session.capacity,
revenueCents: session.sold * session.priceCents
});
} else if (availableRatio < LOW_CAPACITY_THRESHOLD && availableRatioBefore >= LOW_CAPACITY_THRESHOLD) {
// Only when CROSSING the threshold, not on every subsequent sale.
this.emit('low-capacity', {
sessionId,
eventId: session.eventId,
title: session.title,
availableRemaining: availableAfter,
availablePercentage: Math.round(availableRatio * 100)
});
}
return tickets;
}
}
module.exports = { SalesManager, LOW_CAPACITY_THRESHOLD };That last line,
module.exports, is what turns the file into a reusable module. We will study it thoroughly in the CommonJS Modules and require() lesson; for now, accept that it exposes the class so other files can use it withrequire.
Now the consumers. Create src/lab/sale-with-events.js:
// src/lab/sale-with-events.js
const { SalesManager } = require('../domain/sales-manager.js');
const catalog = [
{
id: 'evt-002',
title: 'Noche de Monologos',
sessions: [
{ id: 'ses-002-1', dateTime: '2026-10-10T21:30:00', capacity: 120, sold: 100, priceCents: 1800 },
{ id: 'ses-002-2', dateTime: '2026-10-11T21:30:00', capacity: 120, sold: 45, priceCents: 1800 }
]
}
];
const manager = new SalesManager(catalog);
// --- Listeners: each one with a single responsibility ---
// 1. Operational logging, on stderr.
manager.on('sale-recorded', ({ sessionId, quantity, amountCents, occupancy }) => {
console.error(
`[sale] ${quantity} tickets for ${sessionId} at ` +
`${(amountCents / 100).toFixed(2)} EUR (${occupancy}% occupied)`
);
});
// 2. Commercial notice to the organizer.
manager.on('low-capacity', ({ title, sessionId, availableRemaining, availablePercentage }) => {
console.error(
`[notice] "${title}" (${sessionId}) at ${100 - availablePercentage}%: ` +
`only ${availableRemaining} tickets left`
);
});
// 3. Session sold out: several independent listeners for the SAME event.
manager.on('session-sold-out', ({ title, sessionId, revenueCents }) => {
console.log(
`SOLD OUT: "${title}" (${sessionId}). ` +
`Revenue: ${(revenueCents / 100).toFixed(2)} EUR`
);
});
manager.on('session-sold-out', ({ sessionId }) => {
console.error(`[catalog] marking ${sessionId} as unavailable`);
});
manager.on('session-sold-out', ({ sessionId }) => {
// Heavy work: ALWAYS delegated, never synchronous inside the listener.
setImmediate(() => {
console.error(`[mail] sold-out notice sent for ${sessionId}`);
});
});
// --- Simulating the on-sale opening ---
console.error(`Manager with ${manager.sessionCount} sessions. The sale begins.`);
console.error('');
manager.recordSale('ses-002-1', 5, 'ord-001'); // 105/120: 12.5% available
manager.recordSale('ses-002-1', 4, 'ord-002'); // 109/120: 9.2% available -> low-capacity
manager.recordSale('ses-002-1', 6, 'ord-003'); // 115/120: still low, does NOT re-emit
manager.recordSale('ses-002-1', 5, 'ord-004'); // 120/120 -> session-sold-out
// A usage error: it is thrown, not emitted.
try {
manager.recordSale('ses-002-1', 1, 'ord-005');
} catch (error) {
console.error(`[error] [${error.code}] ${error.message}`);
}Manager with 2 sessions. The sale begins. [sale] 5 tickets for ses-002-1 at 90.00 EUR (88% occupied) [sale] 4 tickets for ses-002-1 at 72.00 EUR (91% occupied) [notice] "Noche de Monologos" (ses-002-1) at 91%: only 11 tickets left [sale] 6 tickets for ses-002-1 at 108.00 EUR (96% occupied) [sale] 5 tickets for ses-002-1 at 90.00 EUR (100% occupied) SOLD OUT: "Noche de Monologos" (ses-002-1). Revenue: 2160.00 EUR [catalog] marking ses-002-1 as unavailable [error] [INSUFFICIENT_CAPACITY] Insufficient capacity in ses-002-1: you asked for 1 and 0 are left [mail] sold-out notice sent for ses-002-1
Five design decisions worth pointing out:
super()is mandatory and must come before any use ofthis. Without it,EventEmitter's internal machinery is not initialized andemitfails.- Event names in
kebab-case:sale-recorded,low-capacity,session-sold-out. It is the ecosystem convention and it avoids problems when using them as strings. low-capacityis only emitted when crossing the threshold. If it were emitted on every sale below 10 %, the organizer would receive twenty emails. Detecting the transition rather than the state is a detail that separates a useful notice from a nuisance.- Usage errors are thrown, not emitted. Insufficient capacity is an error in the calling code; a sold-out session is a domain occurrence. Confusing the two is the most common design mistake with
EventEmitter. - Three different listeners for
session-sold-out, each with one responsibility. The manager knows none of them, and adding a fourth requires no change to it.
Notice the output order too: [mail] appears at the end, even after the error. That is the setImmediate doing its job: it yields control to the event loop and runs in the check phase, once everything synchronous has finished. Exactly what you learned in the event loop lesson.
- The
error event: the only one that can kill you
error event: the only one that can kill youEventEmitter treats one event name specially: error.
If
'error'is emitted and there is no registered listener,EventEmitterthrows the error. If nobody catches it, the process dies.
// src/lab/error-event.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.emit('any-old-event'); // No listeners: nothing happens, returns false
emitter.emit('error', new Error('The payment gateway is not responding'));
// The process DIES here.
console.log('This is never printed');node:events:496
throw er; // Unhandled 'error' event
^
Error: The payment gateway is not responding
at Object.<anonymous> ...
Emitted 'error' event on EventEmitter instance at:
...
[the process dies with code 1]Why such a drastic design? For the same philosophy that makes unhandled rejections take the process down: an error nobody looks at is worse than a crashed process. A server that stays up ignoring connection errors accumulates corrupt state and fails in incomprehensible ways hours later. Node prefers to fail loudly.
The fix is trivial, and it is mandatory in any emitter that can fail:
emitter.on('error', (error) => {
console.error(`[manager] error: ${error.message}`);
// Here: log, notify, decide. But do NOT ignore.
});
emitter.emit('error', new Error('The payment gateway is not responding'));
// Now it is handled correctly and the process carries on.Applied to SalesManager, for asynchronous failures that cannot be thrown:
// Inside the class: a failure in a background process is announced as an event.
syncWithGateway()
.catch((error) => {
error.code = error.code || 'SYNC_FAILED';
this.emit('error', error);
});And in the consumer, without exception:
const manager = new SalesManager(catalog);
// ALWAYS, before anything else.
manager.on('error', (error) => {
console.error(`[manager] [${error.code || 'ERROR'}] ${error.message}`);
process.exitCode = 1;
});When to throw and when to emit error:
| Situation | What to do |
|---|---|
| Invalid argument, unmet precondition (programmer's failure) | throw |
| Asynchronous failure in a running process (network down, disk full) | emit('error', ...) |
| Expected business occurrence (capacity sold out) | emit('session-sold-out', ...), it is not an error |
In Module 6's Error Handling lesson we will formalize this distinction between programming errors and operational errors.
- Memory leaks through listeners
A registered listener is a live reference: it keeps in memory the function and everything its closure captures. If you register listeners and never remove them, memory grows without stopping. It is the most common memory leak in Node applications.
Node warns you when it smells bad:
// src/lab/listener-leak.js
const EventEmitter = require('node:events');
const manager = new EventEmitter();
// We simulate an HTTP request that registers a listener and forgets to remove it.
function handleRequest(number) {
const requestData = new Array(1000).fill(`request-${number}`);
manager.on('session-sold-out', () => {
// The closure retains requestData: 1000 items per request.
console.log(`Request ${number} notified (${requestData.length} items retained)`);
});
}
for (let i = 1; i <= 12; i++) {
handleRequest(i);
}(node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 session-sold-out listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
The warning fires on the eleventh listener of the same event. It is only a warning: the program keeps working and listeners keep being added. But it is an almost always correct signal that something is being registered and never removed.
The three possible answers
1. The warning is right: there is a leak. Unregister.
function handleRequest(number) {
const onSoldOut = (data) => {
console.log(`Request ${number}: ${data.sessionId} sold out`);
};
manager.on('session-sold-out', onSoldOut);
// When the request finishes, we remove the listener.
return function finish() {
manager.off('session-sold-out', onSoldOut);
};
}
const finish = handleRequest(1);
// ... the request is served ...
finish();2. The occurrence happens only once: use once.
// once unregisters itself. No leak possible.
manager.once('session-sold-out', (data) => {
console.log(`First session sold out today: ${data.sessionId}`);
});3. You genuinely need more than ten listeners: raise the limit deliberately.
// Only if you know that 50 listeners is the correct design, not a patch.
manager.setMaxListeners(50);
// Or globally, for every emitter (rarely justified):
// require('node:events').defaultMaxListeners = 20;Never raise the limit to "get rid of the warning". The warning exists to warn you. If you silence it without understanding why it fires, you have traded an annoying message for a silent leak that will take the process down in production at three in the morning.
Diagnosis
// Inspection tools, useful when the warning fires.
console.log(manager.eventNames());
// [ 'session-sold-out', 'sale-recorded', 'error' ]
console.log(manager.listenerCount('session-sold-out'));
// 12
console.log(manager.listeners('session-sold-out').map((f) => f.name));
// [ 'onSoldOut', 'onSoldOut', ... ] <- the same one 12 times: suspiciousIf on a server you see listenerCount growing with the number of requests served, you have a confirmed leak. It is exactly the kind of problem detected by the heapUsed trend from the module's first lesson.
events.once(): awaiting an event with await
events.once(): awaiting an event with awaitSometimes you only want to wait for something to happen once. With the listener API that forces you to wrap everything in a callback. The events module offers a bridge to promises:
// src/lab/wait-for-event.js
const { once } = require('node:events');
const { SalesManager } = require('../domain/sales-manager.js');
const manager = new SalesManager(catalog);
async function waitForFirstSoldOut() {
console.log('Waiting for the first session to sell out...');
// once() returns a PROMISE that fulfills with an ARRAY:
// the arguments the event was emitted with.
const [data] = await once(manager, 'session-sold-out');
console.log(`Sold out: ${data.title} (${data.sessionId})`);
return data;
}
waitForFirstSoldOut().catch((error) => {
console.error(`Failure: ${error.message}`);
process.exitCode = 1;
});
// We trigger the sell-out a little later.
setTimeout(() => {
manager.recordSale('ses-002-1', 20, 'ord-100');
}, 300);Important details of events.once():
- It returns an array, because
emitcan pass several arguments. That is why it is destructured withconst [data] = .... - It rejects automatically if the emitter emits
'error'while waiting. That behavior is very convenient: you do not have to manage two cases by hand. - It accepts a cancellation signal so you do not wait indefinitely:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const [data] = await once(manager, 'session-sold-out', { signal: controller.signal });
console.log(`Sold out: ${data.sessionId}`);
} catch (error) {
if (error.name === 'AbortError') {
console.error('No session sold out within 5 seconds');
} else {
throw error;
}
}And its companion events.on(), which returns an async iterator for consuming a stream of events with for await:
const { on } = require('node:events');
async function followSales(manager) {
// Consumes ALL sales as they happen.
for await (const [data] of on(manager, 'sale-recorded')) {
console.log(`${data.quantity} tickets for ${data.sessionId}`);
}
}Careful with
for await: this loop never ends on its own. It needs a cancellation signal or abreak. And while you process one event, those that arrive pile up in an internal buffer; if your processing is slower than the emission, that buffer grows without bound.
- Events, callbacks or promises: a decision table
You now have all three tools. Choosing well is a design decision, not a matter of taste.
| Criterion | Callback | Promise / async-await |
EventEmitter |
|---|---|---|---|
| Number of results | One (per call) | Exactly one | Many, over time |
| Number of interested parties | One | Many (several .then) |
Many, and unknown |
| Who decides what happens next | The caller | The caller | Each listener, on its own |
| Coupling | Direct | Direct | None |
| Execution | Asynchronous | Asynchronous (microtasks) | Synchronous |
| Error handling | (error, ...) |
try/catch |
The error event (or death!) |
Can it be awaited with await? |
With promisify |
Natively | With events.once() |
| You can arrive late | No | Yes (the promise keeps the result) | No (you miss what was emitted) |
And the practical guide:
| If your case is… | Use |
|---|---|
| "Give me event evt-002" | A promise |
| "Charge this order and tell me whether it worked" | A promise |
| "Tell me every time a chunk of data arrives" | EventEmitter (streams) |
| "Tell me when a session sells out, whenever that is" | EventEmitter |
| "I want several parts of the system to react without knowing each other" | EventEmitter |
| "The Node API only accepts a callback" | A callback (or promisify) |
| "I need to react to what happens, exactly once" | events.once() + await |
The one-line rule:
One answer → a promise. Many notifications → an event.
And an antipattern to avoid: do not use events for the main flow of an operation. Emitting 'order-created' and expecting a listener to continue the purchase flow turns a linear, debuggable process into an invisible chain of jumps. Events are for decoupled side effects — notifying, logging, measuring — not for chopping up something that should read top to bottom.
- All of Node is an
EventEmitter
EventEmitterWhen you understand EventEmitter you unlock half the standard library, because almost everything inherits from it:
// The process object: an EventEmitter.
process.on('exit', (code) => console.error(`Exiting with code ${code}`));
process.on('SIGINT', () => {
console.error('Ctrl+C received: shutting down gracefully...');
process.exit(0);
});
process.on('unhandledRejection', (reason) => console.error(reason));// An HTTP server: an EventEmitter. (Module 4)
const http = require('node:http');
const server = http.createServer();
server.on('request', (request, response) => {
response.end('Escena Viva');
});
server.on('listening', () => console.error('Server ready'));
server.on('close', () => console.error('Server closed'));
server.on('error', (error) => console.error(error.message));// A read stream: an EventEmitter. (Module 3)
const fs = require('node:fs');
const reader = fs.createReadStream('data/sales.csv');
reader.on('data', (chunk) => console.error(`Received ${chunk.length} bytes`));
reader.on('end', () => console.error('File complete'));
reader.on('error', (error) => console.error(`Read error: ${error.message}`));| Node object | Common events | Course module |
|---|---|---|
process |
exit, SIGINT, uncaughtException, unhandledRejection |
2 and 11 |
| HTTP server | request, listening, close, error |
4 |
| Streams | data, end, error, close, finish |
3 |
| TCP sockets | connect, data, end, error |
4 |
| Child processes | exit, close, message, error |
10 |
fs.watch |
change, rename |
3 |
This means what you learned here is not specific to SalesManager: it is the vocabulary Node speaks to you with in every remaining module. When in Module 3 you read reader.on('data', ...), you will already know it is a synchronous listener in a list, that registration order matters, that a slow listener blocks the process and that the error event must always be registered.
Common Mistakes and Tips
Mistake 1: not registering a listener for error.
The only event that kills the process if nobody listens for it. On every emitter that can fail: emitter.on('error', ...).
Mistake 2: believing emit is asynchronous.
It is completely synchronous. A slow listener blocks the main thread and all the other listeners.
Mistake 3: doing heavy work inside a listener.
Delegate with setImmediate or with a genuinely asynchronous operation, and do not forget the .catch.
Mistake 4: an async listener without a .catch.
emit does not wait for the promise and does not catch its rejection. An unhandled rejection takes the process down.
Mistake 5: trying to remove a listener with a different function.
off compares by reference. Store the function in a variable if you are going to unregister it.
Mistake 6: forgetting super() when inheriting from EventEmitter.
ReferenceError: Must call super constructor or, worse, a half-initialized emitter.
Mistake 7: raising setMaxListeners to silence the warning.
It trades an uncomfortable message for a silent leak. Investigate first why they are piling up.
Mistake 8: emitting an event in the constructor.
Nobody has been able to subscribe yet. If you have to do it, defer it with process.nextTick, as you saw in the event loop lesson.
Mistake 9: using events for the main flow. It turns linear code into invisible jumps that are impossible to follow. Events are for decoupled side effects.
Tip 1: event names in kebab-case and in the past tense. sale-recorded, session-sold-out: they describe something that has already happened, which is the correct semantics of an event.
Tip 2: always emit a single object. Adding fields breaks nobody and listeners destructure what they need.
Tip 3: emit on transitions, not on states. low-capacity only when crossing the threshold, never on every subsequent sale.
Tip 4: document your classes' events. Events are part of the public API, just as much as methods. If they are not documented, nobody will use them.
Exercises
Exercise 1: a dashboard as a listener
Write src/lab/sales-dashboard.js with a SalesDashboard class that does not inherit from EventEmitter but instead subscribes to a SalesManager, and that maintains:
- Total tickets sold and accumulated revenue in cents.
- A list of sold-out sessions and a list of sessions on a low-capacity notice.
- A
summary()method returning an object with that data and the revenue formatted in euros. - A
disconnect()method that unregisters all of its listeners from the manager.
Technical requirements:
- The listeners must be arrow functions or bound methods, so that
thisis still the dashboard. disconnect()must leavemanager.listenerCount()at the same value it had before the dashboard connected. Demonstrate it by printing the counters before, during and after.- Simulate a run of sales that sells out a session and verify the summary.
Exercise 2: the error event and the leak warning
Write src/lab/robust-emitter.js that demonstrates, in a single program and without the process dying:
- That emitting
'error'without a listener would throw an exception. Catch it with atry/catcharound theemitand explain in a comment whytry/catchdoes work there (hint:emitis synchronous). - That with an
'error'listener registered the process carries on normally. - That registering 11 listeners of the same event produces a
MaxListenersExceededWarning. Capture it withprocess.on('warning', ...)and print the warning's name instead of letting it print by default. - That after
setMaxListeners(20)the warning no longer appears. - A short final diagnostic with
eventNames()andlistenerCount()for each event.
Exercise 3: awaiting the sell-out with await and a timeout
Write src/lab/wait-for-sold-out.js that:
- Creates a
SalesManagerwith the three sessions ofevt-002(capacities 120, initial sales 118, 45 and 12). - Implements
waitForSoldOut(manager, limitMs)usingevents.once()with anAbortControllerso it never waits longer thanlimitMs. - Simulates random sales every 100 ms on random sessions, with quantities from 1 to 5, until something sells out or the limit expires.
- Reports the result: which session sold out and after how many milliseconds, or that the timeout expired.
- Handles the
AbortErrorcorrectly (a clear message,process.exitCode = 1) and any other error (rethrow it). - Cleans up the timers at the end so the process exits on its own.
Also answer this: why is events.once() better here than registering a once with a callback? And what would happen if the session sold out before calling waitForSoldOut?
Solutions
Solution 1
// src/lab/sales-dashboard.js
// An event consumer that does not inherit from EventEmitter: it only listens.
const { SalesManager } = require('../domain/sales-manager.js');
class SalesDashboard {
#manager;
#connected = false;
constructor(manager) {
this.#manager = manager;
this.ticketsSold = 0;
this.revenueCents = 0;
this.soldOut = [];
this.warned = [];
// We keep the references: they are needed in order to unregister.
// Arrows keep "this" pointing at the dashboard.
this.onSaleRecorded = ({ quantity, amountCents }) => {
this.ticketsSold += quantity;
this.revenueCents += amountCents;
};
this.onSoldOut = ({ sessionId, title }) => {
this.soldOut.push({ sessionId, title });
};
this.onLowCapacity = ({ sessionId, availableRemaining }) => {
this.warned.push({ sessionId, availableRemaining });
};
this.connect();
}
connect() {
if (this.#connected) return;
this.#manager.on('sale-recorded', this.onSaleRecorded);
this.#manager.on('session-sold-out', this.onSoldOut);
this.#manager.on('low-capacity', this.onLowCapacity);
this.#connected = true;
}
disconnect() {
if (!this.#connected) return;
this.#manager.off('sale-recorded', this.onSaleRecorded);
this.#manager.off('session-sold-out', this.onSoldOut);
this.#manager.off('low-capacity', this.onLowCapacity);
this.#connected = false;
}
summary() {
return {
ticketsSold: this.ticketsSold,
revenueEuros: (this.revenueCents / 100).toFixed(2),
soldOutSessions: this.soldOut.length,
warnedSessions: this.warned.length,
soldOut: this.soldOut.map((s) => s.sessionId)
};
}
}
// --- Demonstration ---
const catalog = [
{
id: 'evt-002',
title: 'Noche de Monologos',
sessions: [
{ id: 'ses-002-1', dateTime: '2026-10-10T21:30:00', capacity: 120, sold: 100, priceCents: 1800 },
{ id: 'ses-002-2', dateTime: '2026-10-11T21:30:00', capacity: 120, sold: 45, priceCents: 1800 }
]
}
];
const manager = new SalesManager(catalog);
function counters(label) {
console.error(
`${label.padEnd(24)} sale-recorded=${manager.listenerCount('sale-recorded')} ` +
`session-sold-out=${manager.listenerCount('session-sold-out')} ` +
`low-capacity=${manager.listenerCount('low-capacity')}`
);
}
counters('Before connecting:');
const dashboard = new SalesDashboard(manager);
counters('Dashboard connected:');
manager.recordSale('ses-002-1', 8, 'ord-001'); // 108/120 -> low-capacity
manager.recordSale('ses-002-1', 12, 'ord-002'); // 120/120 -> sold out
manager.recordSale('ses-002-2', 10, 'ord-003');
console.log('');
console.log('DASHBOARD SUMMARY');
console.table([dashboard.summary()]);
dashboard.disconnect();
counters('After disconnecting:');
// One more sale: the dashboard must no longer find out.
manager.recordSale('ses-002-2', 5, 'ord-004');
console.log('');
console.log(`Tickets after disconnecting (must not change): ${dashboard.summary().ticketsSold}`);Before connecting: sale-recorded=0 session-sold-out=0 low-capacity=0 Dashboard connected: sale-recorded=1 session-sold-out=1 low-capacity=1 DASHBOARD SUMMARY ┌─────────┬─────────────┬──────────────┬─────────────────┬────────────────┬───────────────┐ │ (index) │ ticketsSold │ revenueEuros │ soldOutSessions │ warnedSessions │ soldOut │ ├─────────┼─────────────┼──────────────┼─────────────────┼────────────────┼───────────────┤ │ 0 │ 30 │ '540.00' │ 1 │ 1 │ ['ses-002-1'] │ └─────────┴─────────────┴──────────────┴─────────────────┴────────────────┴───────────────┘ After disconnecting: sale-recorded=0 session-sold-out=0 low-capacity=0 Tickets after disconnecting (must not change): 30
The key is keeping the function references in properties of the dashboard. If the listeners had been registered as anonymous arrows inside connect(), disconnect() would have nothing to pass to off and the counters would not return to zero. This discipline is exactly what prevents the memory leaks of section 7.
Solution 2
// src/lab/robust-emitter.js
// Demonstrates the behavior of the 'error' event and of the listener leak warning.
const EventEmitter = require('node:events');
// We intercept Node's warnings to handle them ourselves (point 3).
process.on('warning', (warning) => {
console.error(`[warning captured] ${warning.name}: ${warning.message.split('.')[0]}.`);
});
// --- 1. Emitting 'error' with no listener ---
console.log('1. Emitting "error" with no listener registered');
const emitterWithoutListener = new EventEmitter();
// The try/catch DOES work here because emit is SYNCHRONOUS: the exception is
// thrown within the very call stack we are on.
try {
emitterWithoutListener.emit('error', new Error('The payment gateway is not responding'));
console.log(' This is not printed.');
} catch (error) {
console.log(` Caught: ${error.message}`);
console.log(' Without this try/catch, the process would have died.');
}
// --- 2. With a listener registered ---
console.log('');
console.log('2. Emitting "error" WITH a listener registered');
const emitterWithListener = new EventEmitter();
emitterWithListener.on('error', (error) => {
console.log(` Error listener: ${error.message}`);
});
emitterWithListener.emit('error', new Error('Transient network failure'));
console.log(' The process carries on as normal.');
// --- 3. Listener leak warning ---
console.log('');
console.log('3. Registering 11 listeners of the same event');
const leakyEmitter = new EventEmitter();
for (let i = 1; i <= 11; i++) {
leakyEmitter.on('session-sold-out', () => {});
}
console.log(` Listeners registered: ${leakyEmitter.listenerCount('session-sold-out')}`);
// --- 4. With the limit raised ---
console.log('');
console.log('4. With setMaxListeners(20), no warning');
const wideEmitter = new EventEmitter();
wideEmitter.setMaxListeners(20);
for (let i = 1; i <= 15; i++) {
wideEmitter.on('session-sold-out', () => {});
}
console.log(` Listeners registered: ${wideEmitter.listenerCount('session-sold-out')} (no warning)`);
// --- 5. Diagnostic ---
console.log('');
console.log('5. Diagnostic of emitterWithListener');
emitterWithListener.on('sale-recorded', () => {});
emitterWithListener.on('sale-recorded', () => {});
emitterWithListener.on('low-capacity', () => {});
for (const name of emitterWithListener.eventNames()) {
console.log(` ${String(name).padEnd(20)} ${emitterWithListener.listenerCount(name)} listener(s)`);
}1. Emitting "error" with no listener registered Caught: The payment gateway is not responding Without this try/catch, the process would have died. 2. Emitting "error" WITH a listener registered Error listener: Transient network failure The process carries on as normal. 3. Registering 11 listeners of the same event [warning captured] MaxListenersExceededWarning: Possible EventEmitter memory leak detected. Listeners registered: 11 4. With setMaxListeners(20), no warning Listeners registered: 15 (no warning) 5. Diagnostic of emitterWithListener error 1 listener(s) sale-recorded 2 listener(s) low-capacity 1 listener(s)
The answer to point 1 is the most instructive part of the whole exercise: try/catch works around an emit precisely because emit is synchronous. It is the exception to the rule from the callbacks lesson. If EventEmitter dispatched asynchronously, that try/catch would be as useless as the one that wrapped a setTimeout.
Solution 3
// src/lab/wait-for-sold-out.js
// Awaits the first sell-out with await and a timeout.
const { once } = require('node:events');
const { SalesManager } = require('../domain/sales-manager.js');
const catalog = [
{
id: 'evt-002',
title: 'Noche de Monologos',
sessions: [
{ id: 'ses-002-1', dateTime: '2026-10-10T21:30:00', capacity: 120, sold: 118, priceCents: 1800 },
{ id: 'ses-002-2', dateTime: '2026-10-11T21:30:00', capacity: 120, sold: 45, priceCents: 1800 },
{ id: 'ses-002-3', dateTime: '2026-10-17T21:30:00', capacity: 120, sold: 12, priceCents: 1500 }
]
}
];
const SESSION_IDS = ['ses-002-1', 'ses-002-2', 'ses-002-3'];
// 2. Awaits the event with a timeout by means of AbortController.
async function waitForSoldOut(manager, limitMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), limitMs);
try {
const [data] = await once(manager, 'session-sold-out', { signal: controller.signal });
return data;
} finally {
// Without this clearTimeout the timer would keep the process alive.
clearTimeout(timer);
}
}
async function main() {
const manager = new SalesManager(catalog);
// Mandatory: without an 'error' listener a failure would kill the process.
manager.on('error', (error) => {
console.error(`[manager] ${error.message}`);
});
manager.on('low-capacity', ({ sessionId, availableRemaining }) => {
console.error(`[notice] ${sessionId}: ${availableRemaining} tickets left`);
});
const start = Date.now();
// 3. Random sales every 100 ms.
const simulator = setInterval(() => {
const sessionId = SESSION_IDS[Math.floor(Math.random() * SESSION_IDS.length)];
const quantity = 1 + Math.floor(Math.random() * 5);
try {
manager.recordSale(sessionId, quantity, `ord-${Date.now()}`);
console.error(`[sale] ${quantity} tickets for ${sessionId}`);
} catch (error) {
// Insufficient capacity is normal in a simulation: we ignore it.
if (error.code !== 'INSUFFICIENT_CAPACITY') throw error;
}
}, 100);
try {
const data = await waitForSoldOut(manager, 5000);
// 4. Result.
console.log('');
console.log(`SOLD OUT: "${data.title}" (${data.sessionId})`);
console.log(` Capacity : ${data.capacity}`);
console.log(` Revenue : ${(data.revenueCents / 100).toFixed(2)} EUR`);
console.log(` Time : ${Date.now() - start} ms`);
} catch (error) {
// 5. Distinct handling of the AbortError.
if (error.name === 'AbortError') {
console.error('');
console.error('No session sold out within 5 seconds. The timeout expired.');
process.exitCode = 1;
return;
}
throw error;
} finally {
// 6. Cleanup: without this the setInterval keeps the process alive.
clearInterval(simulator);
}
}
main().catch((error) => {
console.error(`Fatal failure: ${error.message}`);
process.exitCode = 1;
});[sale] 3 tickets for ses-002-3 [sale] 2 tickets for ses-002-2 [notice] ses-002-1: 2 tickets left [sale] 0 tickets for ses-002-1 [sale] 4 tickets for ses-002-3 [sale] 2 tickets for ses-002-1 SOLD OUT: "Noche de Monologos" (ses-002-1) Capacity : 120 Revenue : 2160.00 EUR Time : 612 ms
Answers to the questions:
- Why
events.once()is better here: because the flow is "wait for a result and carry on", which is exactly the shape of a promise. With amanager.once('session-sold-out', callback)you would have to put all the following code inside the callback, losetry/catchand manage the timeout by hand with asetTimeoutthat would also have to unregister the listener.events.once()with anAbortSignalsolves all four things at once, and it rejects automatically if the manager emitserror. - If the session sold out before calling
waitForSoldOut, the event would have been lost forever:waitForSoldOutwould sit waiting until its 5-second limit ran out. An event has no memory, unlike a promise, which keeps its result for anyone who subscribes later. It is the most important difference between the two mechanisms and the reason emitters must never emit in their constructor: you have to give people time to subscribe.
Conclusion
You have taken on the third piece of asynchrony in Node. The observer pattern lets an object announce that something has happened without knowing who reacts, and EventEmitter is its implementation in Node's core: on to subscribe, once for a single time, emit to announce, off to unregister, and listenerCount/eventNames to diagnose.
You have discovered the fact that surprises most and has the most consequences: listeners run synchronously, in registration order, and emit does not return until they have all finished. Hence the rule that a listener must be fast and delegate heavy work with setImmediate or with an asynchronous operation that always carries its .catch. And hence, too, the one exception to what you learned in the callbacks lesson: around an emit, try/catch does work.
You have built Module 1's commitment: src/domain/sales-manager.js, with the SalesManager extends EventEmitter class that emits sale-recorded on every operation, low-capacity when crossing the 10 %-available threshold — not on every subsequent sale — and session-sold-out when there are no tickets left, with three independent listeners subscribed to the same event and none of them known to the manager. And you have settled the design distinction that matters: usage errors are thrown, domain occurrences are emitted, and asynchronous failures go to the error event.
You know that error is the only event that kills the process if nobody listens for it, and why Node prefers to fail loudly rather than accumulate corrupt state. You know listeners are live references and that a MaxListenersExceededWarning almost always points at a real leak — never silenced by raising the limit without investigating. And you have the bridge to the previous lesson: events.once() returns a promise you can await with await, with an AbortSignal for the timeout and automatic rejection on an error. The rule that sums it all up: one answer → a promise; many notifications → an event.
Finally, you have seen that this is not a corner of the language: process, the HTTP server, sockets, streams and child processes are all EventEmitters. What you learned here is the vocabulary of the ten remaining modules.
And yet, there is something we have been dragging along from the start. In every lab file you have copied the catalog array again. You have written module.exports three times without anyone explaining exactly what it does. And the promise to turn src/catalog-data.js into a reusable module and to move the Event and Session classes into src/domain/ is still outstanding. In the next lesson, CommonJS Modules and require(), we will settle that debt: you will understand the wrapper Node adds to every file, why exports = ... does not work and module.exports = ... does, how require finds what it is looking for, and why a module is really a singleton.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
