Patrones de EventEmitter
Usa Node.js EventEmitter para patrones de publicación-suscripción, con eventos tipados y manejadores asincronos.
Busca en todas las páginas de la documentación
Usa Node.js EventEmitter para patrones de publicación-suscripción, con eventos tipados y manejadores asincronos.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
// Suscribirse
emitter.on("message", (text: string) => {
console.log("got:", text);
});
// Suscribirse una única vez
emitter.once("ready", () => {
console.log("ready fired once");
});
// Desuscribirse
const handler = (n: number) => console.log(n);
emitter.on("tick", handler);
emitter.off("tick", handler);
// Publicar
emitter.emit("message", "hello");
emitter.emit("ready");
// Siempre maneja errores - unhandled 'error' bloquea el proceso
emitter.on("error", (err) => {
console.error("emitter error:", err);
});
emitter.emit("error", new Error("boom"));Cuándo usarlo: Desacoplamiento de productores de consumidores dentro de un único proceso de Node - colas de trabajo, tuberías de registro, ciclo de vida de stream, hooks de plugin.
Una clase envolvente de EventEmitter tipada para una cola de trabajo.
import { EventEmitter } from "node:events";
// Mapa de tipos: nombre de evento a tupla de argumentos
interface JobEvents {
"job:start": [id: string];
"job:progress": [id: string, percent: number];
"job:complete": [id: string, result: unknown];
"job:error": [id: string, err: Error];
}
class JobQueue extends EventEmitter {
override on<K extends keyof JobEvents>(
event: K,
listener: (...args: JobEvents[K]) => void
): this {
return super.on(event, listener as (...args: unknown[]) => void);
}
override emit<K extends keyof JobEvents>(
event: K,
...args: JobEvents[K]
): boolean {
return super.emit(event, ...args);
}
async run(id: string, task: (report: (p: number) => void) => Promise<unknown>) {
this.emit("job:start", id);
try {
const result = await task((percent) => {
this.emit("job:progress", id, percent);
});
this.emit("job:complete", id, result);
} catch (err) {
this.emit("job:error", id, err as Error);
}
}
}
const queue = new JobQueue();
queue.on("job:start", (id) => console.log(`[${id}] start`));
queue.on("job:progress", (id, pct) => console.log(`[${id}] ${pct}%`));
queue.on("job:complete", (id, result) => console.log(`[${id}] done`, result));
queue.on("job:error", (id, err) => console.error(`[${id}] failed`, err));
await queue.run("job-1", async (report) => {
for (let i = 0; i <= 100; i += 25) {
report(i);
await new Promise((r) => setTimeout(r, 50));
}
return { ok: true };
});Lo que esto demuestra:
on y emit proporcionan autocompletado total y seguridad de carga útil.run) y los consumidores (los manejadores on) están completamente desacoplados.EventEmitter mantiene un mapa interno de nombre de evento a un array de funciones listener. emit itera ese array sincrónic amente, llamando a cada listener en el orden en que fue registrado. No hay cola incorporada ni contrapresión - los listeners se ejecutan en el tick actual a menos que ellos mismos programen trabajo asincrónico.
on añade, once añade un envolvente que se elimina a sí mismo después de la primera llamada, off (alias removeListener) elimina por referencia, y removeAllListeners(event?) borra todo.
El evento 'error' es especial: si nada está escuchando cuando haces emit('error', err), Node lanza el error y, si no se atrapa, termina el proceso.
EventEmitter tipado con generics. El patrón JobQueue anterior es el enfoque idiomático en Node + TypeScript modernos. Las bibliotecas como tsee o typed-emitter empaquetan la misma idea.
Iteración asincrónica con events.on(). Consume eventos como un stream:
import { on, EventEmitter } from "node:events";
const emitter = new EventEmitter();
setTimeout(() => emitter.emit("data", 1), 10);
setTimeout(() => emitter.emit("data", 2), 20);
for await (const [value] of on(emitter, "data")) {
console.log("value:", value);
if (value === 2) break;
}once como una Promise. Excelente para esperar un único evento del ciclo de vida:
import { once, EventEmitter } from "node:events";
const emitter = new EventEmitter();
setTimeout(() => emitter.emit("ready", "ok"), 50);
const [value] = await once(emitter, "ready");
console.log(value); // "ok"Extendiendo EventEmitter. Subclas ifica cuando el emitter es la identidad principal de tu objeto (el ejemplo JobQueue). Compón (ten un #emitter interno) cuando los eventos son una preocupación secundaria.
Eliminando todos los listeners. emitter.removeAllListeners() sin argumento elimina todos los eventos; con un argumento, solo ese evento. Útil en desmontaje pero peligroso si no posees el emitter.
Advertencia de máximo de listeners. Node imprime MaxListenersExceededWarning con 11 listeners por evento. Elévalo con emitter.setMaxListeners(50) o globalmente a través de EventEmitter.defaultMaxListeners. La advertencia es una pista de fuga, no un error.
Define un mapa de tipo de evento como una interfaz donde cada clave es el nombre del evento y cada valor es una tupla de los argumentos del listener - por ejemplo interface Events \{ start: [id: string]; progress: [percent: number] \}. Usa este mapa con una clase base genérica como TypedEmitter<Events> (de typed-emitter), o escribe tus propias anulaciones de on, once, off, y emit que restrinjan el nombre del evento con K extends keyof Events y los argumentos con Events[K]. Esto te da autocompletado en nombres de eventos y verificación de compilación de formas de carga útil.
error bloquea el proceso. Emitir 'error' sin listener lanza. Siempre adjunta un listener de error antes de cualquier ruta de código que pueda emitir uno.emit es sincrónico. Los listeners se ejecutan en orden de registro en el mismo tick. Un listener sincrónico lento bloquea todos los demás listeners y el event loop.once no previene otras registraciones. Solo envuelve ese único manejador. Otros listeners on siguen disparándose para siempre.emitter.off("x", () => {}) pasa una referencia de función nueva y no elimina nada. Almacena el manejador en una variable si alguna vez necesitas eliminarlo.emit y puede derribar listeners no relacionados. Envuelve trabajo riesgoso en try/catch o usa manejadores asincronos que rechacen en un evento de error.emit no espera nada. Las promesas rechazadas en listeners se convierten en rechazos no manejados a menos que adjuntes .catch dentro del listener.| Opción | Mejor Para | Notas |
|---|---|---|
node:events EventEmitter | Servicios solo de Node | Incorporado, cero dependencias, emit sincrónico. |
EventTarget / CustomEvent | Universal (Node 19+, navegadores, workers) | Estándar web, un poco más verboso, soporta AbortSignal para limpieza. |
mitt | Pub-sub universal minúsculo | ~200 bytes, sin comodín, sin once - solo on/off/emit. |
nanoevents | Emitter tipado pequeño | Genéricos de TypeScript de primera clase, devuelve una función de desuscripción. |
RxJS Subject | Streams, operadores, contrapresión | Pesado, pero inigualable para tuberías asincrónicas complejas. |
| Streams de Node | Streams de bytes/objetos con contrapresión | Usa cuando el orden y el control de flujo importan, no solo señalización. |
error sin un listener?Node lanza el error sincrónic amente desde emit. Si nada lo atrapa, el proceso se bloquea con un mensaje Unhandled 'error' event. Siempre adjunta un listener de error antes del primer emit.
emit sincrónico o asincrónico?Sincrónico. Los listeners se ejecutan en orden de registro en el tick actual, bloqueando el event loop hasta que regresan. Los listeners asincronos son dispara y olvida - emit no los espera.
once?Guarda la referencia y pásala a off: const h = () => {}; emitter.once("x", h); emitter.off("x", h). También puedes llamar a removeAllListeners("x") si posees el emitter.
MaxListenersExceededWarning se dispara con 11 listeners en un único evento - una heurística para detectar fugas. Si realmente necesitas más, llama a emitter.setMaxListeners(50) o establece EventEmitter.defaultMaxListeners globalmente.
Define una interfaz mapeando nombres de eventos a tuplas de argumentos, luego subclas ifica EventEmitter y anula on y emit con firmas genéricas restringidas por K extends keyof YourEvents. Ve el ejemplo práctico de JobQueue en esta página.
Extiende cuando los eventos son la identidad principal del objeto (un bus, una cola, un stream). Compón con un campo privado #emitter cuando los eventos son una preocupación secundaria - eso mantiene tu API pública más pequeña y evita exponer todos los métodos de EventEmitter.
Sí - usa events.once(emitter, "name") que devuelve una promise que se resuelve a los argumentos emitidos como un array. También rechaza en 'error', lo que la hace más segura que un envolvente hecho a mano.
Usa events.on(emitter, "name") que devuelve un iterador asincrónico. Puedes hacer for await sobre él y break para dejar de escuchar. Empareja con una opción AbortSignal para cancelación limpia.
off y removeListener?Ninguna - off es un alias agregado para paridad con el DOM. Ambos eliminan un listener por referencia.
emitter.off("x", () => {}) crea una función nueva y la pasa a off, que no encuentra coincidencia. Almacena el manejador en una variable o usa once si solo lo necesitas una vez.
EventTarget con CustomEvent es el estándar web y funciona en todas partes, incluido Node 19+. Para pub-sub universal ultra pequeño, alcanza mitt o nanoevents. No envíes node:events al navegador.
Envuelve el cuerpo asincrónico en try/catch y reenvía fallos al emitter: emitter.on("x", async (v) => { try { await work(v); } catch (err) { emitter.emit("error", err); } }). emit no esperará tu manejador, así que debes atrapar adentro.
docs/nodejs-scripts/http-server.md - Servidores HTTP, que son instancias de EventEmitter.docs/browser-apis/postmessage-basics.md - mensajería entre ventanas, un patrón pub-sub del lado del navegador.docs/react-hooks/use-effect.md - suscripción y limpieza de listeners de eventos desde React.Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥