EventEmitter es el patrón que Node utiliza para permitir que una parte de un programa anuncie que algo ha sucedido, sin necesidad de saber quién, si es que alguien, está escuchando. Es una de las APIs más antiguas en el módulo node:events de Node, y una de las más fundamentales: streams, servidores HTTP, sockets y procesos hijos son todos, debajo de sus APIs específicas, EventEmitters.
EventEmitter es un mecanismo de publicación/suscripción síncrono y en proceso: un objeto mantiene una lista de eventos con nombre, y cualquier número de funciones listener puede suscribirse a cada nombre.
Por qué es importante: Desacopla el código que detecta que algo sucede del código que reacciona a ello, sin necesidad de que ninguna de las partes se conozca directamente.
Conceptos clave:emisor, listener, nombre del evento, emit, despacho síncrono, desacoplamiento.
Cuándo usarlo: Modelar notificaciones en proceso (un trabajo terminado, una conexión cerrada, un valor cambiado), construir un sistema de plugins/hooks, o trabajar con cualquier API central de Node que ya exponga eventos (streams, servidores, sockets).
Limitaciones / Compromisos: EventEmitter es completamente en memoria y síncrono por llamada de listener; no tiene persistencia, ni entrega entre procesos, y un listener lento o que lanza un error afecta directamente al llamador del emisor.
Temas relacionados: API de streams de Node, el bucle de eventos y la pila de llamadas, colas de mensajes, el patrón de diseño observador.
La idea central detrás de EventEmitter es el patrón observador: un objeto (el emisor) expone eventos con nombre, otro código registra funciones listener contra esos nombres, y llamar a emit(nombre, ...) ejecuta cada listener actualmente registrado para ese nombre. Ninguna de las partes necesita una referencia a la otra más allá de la propia instancia compartida del emisor; el emisor no sabe ni le importa quién está escuchando, y un listener no necesita saber qué lo activó más allá del nombre y la carga útil del evento.
Una analogía útil: piensa en un emisor como el sistema de alarma contra incendios de un edificio en lugar de una llamada telefónica. Al tirar de la alarma (emit) no se marca a una persona específica, sino que suena cada campana de alarma registrada (listener) en el edificio a la vez, y la persona que tira de la palanca no necesita saber cuántas personas hay dentro o quiénes son.
import { EventEmitter } from 'node:events';const uploads = new EventEmitter();uploads.on('completed', (fileId: string) => { console.log(`upload finished: ${fileId}`);});uploads.emit('completed', 'file-123'); // ejecuta cada listener de 'completed', en orden
Node adoptó este patrón temprano porque gran parte de lo que hace un servidor tiene inherentemente forma de evento: un socket recibe datos, una solicitud se completa, un proceso hijo termina. En lugar de inventar una convención de callback a medida para cada uno de ellos, Node estandarizó un mecanismo compartido y luego construyó muchas de sus propias clases centrales directamente sobre él.
El hecho mecánico más importante sobre EventEmitter es que es síncrono: llamar a emitter.emit(nombre, ...) invoca a cada listener registrado para ese nombre, uno tras otro, en la pila de llamadas actual, y emit() solo regresa después de que todos ellos hayan regresado. Esto es fundamentalmente diferente de una cola de mensajes o un broker de pub/sub; no hay almacenamiento en búfer, no hay garantía de entrega y ningún listener registrado después de una llamada a emit() recibe esa emisión en particular.
emitter.on('tick', () => console.log('A'));emitter.on('tick', () => console.log('B'));emitter.emit('tick');console.log('C');// Salida: A, B, C - ambos listeners se ejecutan hasta su finalización,// sincrónicamente, antes de que emit() regrese y se registre 'C'.
Esa sincronicidad tiene una consecuencia directa para el manejo de errores: si un listener lanza un error, la excepción se propaga desde emit() mismo, al igual que cualquier otro lanzamiento síncrono; no es capturada ni tragada por el emisor, y detiene la ejecución de cualquier listener posterior para esa misma emisión. Para el evento especial 'error' específicamente, Node añade una regla más: si un emisor emite 'error' sin ningún listener registrado para él, Node lo trata como una excepción no capturada y bloquea el proceso, precisamente porque un error ignorado silenciosamente es peor que uno ruidoso.
El otro detalle mecánico que vale la pena internalizar es por qué tantas APIs centrales de Node extienden EventEmitter en lugar de exponer una forma de callback diferente: le da a cada stream, servidor y socket una forma uniforme de exponer múltiples señales, a las que se puede suscribir de forma independiente ('data', 'error', 'close', 'end', y más) desde un solo objeto, en lugar de necesitar un argumento de constructor o método separado para cada una. Una vez que entiendes el EventEmitter simple, ya entiendes la forma de stream.Readable, net.Server y child_process.ChildProcess: superponen nombres de eventos específicos del dominio sobre el mecanismo exacto descrito anteriormente.
La mayor limitación estructural de EventEmitter es también su compromiso definitorio: es completamente en proceso y en memoria. No hay persistencia (un listener registrado después de que se dispara un evento nunca lo ve), no hay garantía de entrega y no hay durabilidad entre procesos o reinicios; un emisor y sus listeners deben vivir en el mismo proceso en ejecución, compartiendo el mismo espacio de memoria.
Eso lo convierte en una herramienta fundamentalmente diferente de una cola de mensajes o un bus de eventos, aunque ambos a veces se denominan vagamente "eventos". Eventos de dominio vs EventEmitter cubre exactamente dónde se encuentra esa línea: EventEmitter se adapta a notificaciones rápidas, en proceso y de mejor esfuerzo (actualizaciones de UI, hooks internos, señalización de streams), mientras que las colas (Kafka, SQS, RabbitMQ o un patrón de bandeja de salida) se adaptan a cualquier cosa que necesite sobrevivir a un reinicio, cruzar un límite de proceso o garantizar una entrega al menos una vez.
Enfoque
Fortaleza
Debilidad
Mejor ajuste
EventEmitter
Carga casi nula; síncrono, orden predecible; integrado en cada API central relevante
Solo en proceso; sin persistencia; un listener que lanza un error afecta directamente al llamador del emisor
Notificaciones en proceso, hooks de plugins, envolviendo APIs tipo stream
Cola de mensajes (Kafka, SQS, RabbitMQ)
Duradero, sobrevive a reinicios, cruza límites de proceso/servicio, garantías de entrega
Infraestructura real y sobrecarga operativa; asíncrono por naturaleza
Eventos entre servicios, cualquier cosa que no deba perderse
Callback directo / Promesa
Notificación uno a uno más simple posible; sin abstracción adicional
No escala más allá de un consumidor sin distribución manual
Un solo llamador esperando un único resultado específico
Vale la pena mencionar dos modos de fallo operativos porque aparecen repetidamente en los servicios de Node en producción. Primero, la acumulación de listeners: una llamada a on() sin una off()/removeListener() correspondiente mantiene ese listener (y todo lo que cierra) vivo durante toda la vida útil del emisor, lo que es una fuente rutinaria de crecimiento de memoria en procesos de larga duración. La advertencia predeterminada maxListeners de Node (10 por nombre de evento) existe específicamente como un detector de humo temprano para esto, no como un límite estricto. Fugas de memoria de los listeners cubre la limpieza basada en AbortSignal y cuándo aumentar ese límite es legítimo versus una señal de una fuga real.
Segundo, la seguridad de tipos a escala: las firmas emit/on de un EventEmitter simple aceptan cualquier cadena como nombre de evento y cualquier argumento como carga útil, lo que significa que un nombre de evento mal escrito o una forma de carga útil no coincidente es invisible para el compilador de TypeScript por defecto. EventEmitter tipado cubre la restricción de ambos extremos (nombres y formas de carga útil) para que esos errores salgan a la luz en tiempo de compilación en lugar de no hacer nada silenciosamente en tiempo de ejecución.
"emit() es asíncrono, como la mayoría de las otras E/S de Node." Es completamente síncrono: cada listener se ejecuta, en orden de registro, en la pila de llamadas actual, antes de que emit() regrese; nada sobre EventEmitter en sí mismo pospone la ejecución a un tick posterior.
"EventEmitter es básicamente una cola de mensajes ligera." Una cola almacena en búfer y persiste mensajes para una entrega posterior o repetida; EventEmitter no tiene ningún búfer en absoluto: si nada está escuchando cuando se ejecuta emit(), esa emisión simplemente desaparece.
"Un error lanzado dentro de un listener es capturado por el emisor." Se propaga exactamente como cualquier lanzamiento síncrono, interrumpiendo potencialmente a los listeners posteriores para esa misma emisión y apareciendo en el sitio de la llamada a emit(): el emisor no proporciona un try/catch implícito.
"maxListeners es un límite estricto que comenzará a eliminar listeners." Solo activa una advertencia (MaxListenersExceededWarning) una vez superado; es una heurística de detección de fugas, no un límite impuesto, y aumentarlo es a veces la solución correcta en lugar de ser siempre una señal de alerta.
"Solo el código de aplicación personalizado usa EventEmitter; las APIs centrales de Node tienen lo suyo." Lo contrario es cierto: streams, servidores HTTP, sockets y procesos hijos son todos EventEmitters debajo de sus nombres de método específicos, utilizando este mismo mecanismo.
Un mecanismo de publicación/suscripción en proceso donde un objeto mantiene eventos con nombre, y llamar a emit(nombre, ...) ejecuta sincrónicamente cada listener actualmente registrado para ese nombre.
¿Por qué tantas APIs centrales de Node extienden EventEmitter?
Le da a cualquier objeto una forma uniforme de exponer múltiples señales a las que se puede suscribir de forma independiente desde una instancia: los eventos 'data', 'error' y 'end' de un stream utilizan el mismo mecanismo en lugar de necesitar APIs de callback personalizadas separadas.
¿Es `emit()` síncrono o asíncrono?
Síncrono: llama a cada listener registrado para ese nombre de evento, en orden, en la pila de llamadas actual, y solo regresa una vez que todos han terminado de ejecutarse.
¿Qué sucede si un listener lanza una excepción?
La excepción se propaga desde la llamada a emit() como cualquier lanzamiento síncrono normal; no es capturada por el emisor, y puede evitar que cualquier listener registrado después del que lanzó el error (para esa misma emisión) se ejecute.
¿Por qué un evento `'error'` no manejado bloquea el proceso?
Node trata 'error' de forma especial: si un emisor lo emite y no hay ningún listener registrado para ese evento específico, Node lo lanza como una excepción no capturada en lugar de descartarlo silenciosamente, una elección deliberada para hacer que los errores ignorados sean ruidosos en lugar de invisibles.
¿Un listener registrado después de que se ejecuta `emit()` recibe ese evento?
No, EventEmitter no tiene almacenamiento en búfer ni reproducción; una emisión que ocurre antes de que se registre un listener simplemente desaparece para cuando ese listener se suscribe.
¿Cuál es la diferencia entre `on()` y `once()`?
on() registra un listener que se ejecuta cada vez que se dispara el evento; once() registra uno que se ejecuta solo en la siguiente ocurrencia, y luego se elimina automáticamente, útil para señales únicas como "conexión establecida".
¿Es EventEmitter un sustituto de una cola de mensajes?
No, es completamente en proceso y en memoria sin persistencia ni garantía de entrega, mientras que una cola (Kafka, SQS, RabbitMQ) está construida específicamente para la durabilidad y para cruzar límites de proceso o servicio; resuelven problemas diferentes aunque ambos sean coloquialmente "eventos".
¿Por qué Node advierte sobre más de 10 listeners en un evento?
La advertencia predeterminada maxListeners es una heurística de detección de fugas, no un límite estricto: la acumulación de muchos listeners en un nombre de evento es un síntoma común de llamadas a on() olvidadas sin una eliminación correspondiente, por lo que Node lo marca temprano en lugar de dejar que crezca silenciosamente.
¿Pueden dos listeners diferentes para el mismo evento ejecutarse en paralelo?
No, debido a que emit() es síncrono, los listeners para una emisión siempre se ejecutan uno tras otro en la misma pila de llamadas, nunca concurrentemente; la ejecución "paralela" requeriría que cada listener pasara a algo asíncrono internamente.
¿EventEmitter garantiza el orden de ejecución de los listeners?
Sí, para un nombre de evento dado, los listeners se ejecutan en el orden en que fueron registrados; emit() recorre su lista interna de listeners para ese nombre secuencialmente, no en una secuencia arbitraria o reordenada.