Las promesas resolvieron la asincronía de un solo resultado: pides algo, esperas, llega el valor o el error, se acabó. Pero buena parte de lo que ocurre en un sistema real no tiene esa forma. Una sesión que se agota, una venta que se registra, un aforo que baja del 10 %, un socket que recibe datos, un fichero que se modifica: eso no es un resultado futuro, es un flujo de sucesos que ocurren muchas veces y en momentos impredecibles.

Node.js tiene un mecanismo propio para eso, y no es un accesorio: es una de sus piezas fundacionales. El módulo events y su clase EventEmitter están por debajo de los streams, del servidor HTTP, de los sockets, de los procesos hijo y del propio objeto process. Cuando escribes servidor.on('request', ...) estás usando exactamente la misma API que vas a aprender aquí.

En esta lección construirás el GestorDeVentas de Escena Viva: una clase que hereda de EventEmitter y que avisa al resto del sistema cuando se registra una venta, cuando el aforo de una sesión baja del 10 % y —el compromiso que hicimos en el Módulo 1— cuando una sesión se agota. Por el camino descubrirás que los oyentes se ejecutan de forma síncrona, por qué el evento error es especial hasta el punto de tumbar el proceso, cómo se producen las fugas de memoria por oyentes olvidados, y cómo esperar un evento con await.

Contenido

  1. El patrón observador y por qué Node lo lleva en el ADN
  2. La API de EventEmitter
  3. Los oyentes se ejecutan de forma síncrona
  4. Argumentos en emit y el valor de this
  5. Heredar de EventEmitter: el GestorDeVentas de Escena Viva
  6. El evento error: el único que puede matarte
  7. Fugas de memoria por oyentes
  8. events.once(): esperar un evento con await
  9. Eventos, callbacks o promesas: tabla de decisión
  10. Todo Node es un EventEmitter

  1. El patrón observador y por qué Node lo lleva en el ADN

El patrón observador resuelve un problema de diseño concreto: un objeto necesita avisar a otros de que algo ha pasado, sin saber quiénes son ni cuántos hay.

Sin el patrón, el código queda así:

// MAL: el gestor de ventas conoce a todos sus consumidores.
function registrarVenta(sesionId, cantidad) {
  const sesion = buscarSesion(sesionId);
  sesion.vendidas += cantidad;

  if (sesion.vendidas >= sesion.aforo) {
    correo.avisarOrganizador(sesion);        // Acoplamiento 1
    catalogo.marcarComoAgotada(sesion.id);   // Acoplamiento 2
    metricas.contarAgotamiento(sesion.id);   // Acoplamiento 3
    auditoria.registrar('agotada', sesion);  // Acoplamiento 4
  }
}

Cada nueva necesidad —"avisa también al panel del administrador", "publica en la caché"— obliga a modificar la función de ventas, que no tiene nada que ver con correos ni con métricas. Es la receta para que un módulo central acabe importando media aplicación.

Con el patrón observador:

// BIEN: el gestor solo anuncia. No sabe quien escucha.
function registrarVenta(sesionId, cantidad) {
  const sesion = buscarSesion(sesionId);
  sesion.vendidas += cantidad;

  if (sesion.vendidas >= sesion.aforo) {
    this.emit('sesion-agotada', { sesionId, aforo: sesion.aforo });
  }
}

Y cada interesado se suscribe por su cuenta:

gestor.on('sesion-agotada', (datos) => correo.avisarOrganizador(datos));
gestor.on('sesion-agotada', (datos) => metricas.contarAgotamiento(datos.sesionId));
flowchart LR
    G["GestorDeVentas<br/><i>emisor</i>"] -->|"emit('sesion-agotada')"| E{{"Evento"}}
    E --> O1["Aviso por correo<br/>al organizador"]
    E --> O2["Actualizar el<br/>catalogo publico"]
    E --> O3["Registrar metrica"]
    E --> O4["Auditoria"]

    style G fill:#2b6cb0,color:#fff
    style E fill:#805ad5,color:#fff

Los dos roles del patrón:

Rol Quién es Qué hace
Emisor (subject) GestorDeVentas Anuncia que algo ha pasado. No conoce a los oyentes
Oyente (observer) Correo, métricas, auditoría… Se suscribe a los eventos que le interesan

La ganancia es el desacoplamiento: puedes añadir, quitar o probar oyentes sin tocar una sola línea del emisor. En el Módulo 9 esto será decisivo, porque probar el GestorDeVentas no requerirá ningún servicio de correo.

Node lleva este patrón en el ADN por una razón histórica: es la forma natural de expresar E/S asíncrona repetida. Un socket no entrega "un resultado": entrega datos muchas veces, luego se cierra, y en el camino puede fallar. Eso son tres eventos distintos, no una promesa.

  1. La API de EventEmitter

EventEmitter vive en el módulo events del núcleo de Node.

// src/laboratorio/emisor-basico.js
const EventEmitter = require('node:events');

const emisor = new EventEmitter();

// Registrar un oyente
emisor.on('venta-registrada', (datos) => {
  console.log(`Venta de ${datos.cantidad} entradas en ${datos.sesionId}`);
});

// Emitir el evento
emisor.emit('venta-registrada', { sesionId: 'ses-002-2', cantidad: 3 });
// Venta de 3 entradas en ses-002-2

Los métodos que usarás:

Método Qué hace Devuelve
on(evento, oyente) Registra un oyente. Alias: addListener El emisor (encadenable)
once(evento, oyente) Registra un oyente que se ejecuta una sola vez y se elimina El emisor
emit(evento, ...args) Dispara el evento, llamando a todos los oyentes en orden true si había oyentes, false si no
off(evento, oyente) Elimina un oyente concreto. Alias: removeListener El emisor
removeAllListeners([evento]) Elimina todos los oyentes de un evento (o de todos) El emisor
listenerCount(evento) Cuántos oyentes tiene ese evento Número
eventNames() Nombres de todos los eventos con oyentes Array
prependListener(evento, oyente) Registra un oyente al principio de la lista El emisor
setMaxListeners(n) Cambia el umbral del aviso de fuga (por defecto 10) El emisor

on frente a once

// src/laboratorio/on-vs-once.js
const EventEmitter = require('node:events');
const emisor = new EventEmitter();

emisor.on('venta', (n) => console.log(`  [on]   venta numero ${n}`));
emisor.once('venta', (n) => console.log(`  [once] venta numero ${n}`));

for (let i = 1; i <= 3; i++) {
  console.log(`Emitiendo venta ${i} (oyentes: ${emisor.listenerCount('venta')})`);
  emisor.emit('venta', i);
}
Emitiendo venta 1 (oyentes: 2)
  [on]   venta numero 1
  [once] venta numero 1
Emitiendo venta 2 (oyentes: 1)
  [on]   venta numero 2
Emitiendo venta 3 (oyentes: 1)
  [on]   venta numero 3

once se desregistra solo tras la primera ejecución. Es la elección correcta para sucesos que ocurren una única vez: 'listo', 'conectado', 'cerrado'.

Desregistrar oyentes

Para poder quitar un oyente necesitas la misma referencia de función con la que lo registraste:

// MAL: son dos funciones distintas. El off no hace nada.
emisor.on('venta', (n) => console.log(n));
emisor.off('venta', (n) => console.log(n));
console.log(emisor.listenerCount('venta'));   // 1 <- sigue ahi

// BIEN: guardamos la referencia.
function alVender(n) {
  console.log(n);
}
emisor.on('venta', alVender);
emisor.off('venta', alVender);
console.log(emisor.listenerCount('venta'));   // 0

Es una de las causas más comunes de fugas de memoria por oyentes, y volveremos a ella en el apartado 7.

emit te dice si alguien escuchaba

const habiaOyentes = emisor.emit('evento-sin-oyentes');
console.log(habiaOyentes);   // false

Emitir un evento sin oyentes no es un error: simplemente no pasa nada. Con una excepción muy importante, el evento error, que veremos en el apartado 6.

  1. Los oyentes se ejecutan de forma síncrona

Esta es la característica de EventEmitter que más sorprende, y la que tiene consecuencias de rendimiento reales:

emit llama a todos los oyentes de forma síncrona, uno tras otro, en el orden en que se registraron, y no devuelve hasta que todos han terminado.

EventEmitter no es asíncrono. Es un mecanismo de despacho síncrono que resulta que se usa mucho en contextos asíncronos.

// src/laboratorio/oyentes-sincronos.js
const EventEmitter = require('node:events');
const emisor = new EventEmitter();

emisor.on('venta', () => console.log('  oyente 1 (registrado primero)'));
emisor.on('venta', () => console.log('  oyente 2 (registrado segundo)'));
emisor.on('venta', () => console.log('  oyente 3 (registrado tercero)'));

console.log('Antes de emit');
emisor.emit('venta');
console.log('Despues de emit');
Antes de emit
  oyente 1 (registrado primero)
  oyente 2 (registrado segundo)
  oyente 3 (registrado tercero)
Despues de emit

Los tres oyentes se ejecutaron antes de que emit devolviera el control. Nada de bucle de eventos, nada de microtareas.

La implicación para el rendimiento

Si emit es síncrono, un oyente lento bloquea a todos los demás y al hilo principal. Todo lo que aprendiste en la lección de arquitectura se aplica aquí:

// src/laboratorio/oyente-lento.js
const EventEmitter = require('node:events');
const emisor = new EventEmitter();

emisor.on('venta', () => console.log('  rapido 1'));

emisor.on('venta', () => {
  // Un oyente que hace trabajo pesado de forma sincrona.
  const limite = Date.now() + 200;
  while (Date.now() < limite) { /* generar el PDF de la entrada, por ejemplo */ }
  console.log('  LENTO (200 ms)');
});

emisor.on('venta', () => console.log('  rapido 2'));

const inicio = Date.now();
emisor.emit('venta');
console.log(`emit tardo ${Date.now() - inicio} ms`);
  rapido 1
  LENTO (200 ms)
  rapido 2
emit tardo 201 ms

El emit tardó 201 ms. Si eso ocurriera dentro de una petición HTTP de Escena Viva, el servidor entero se detendría 200 ms por cada venta.

La regla de oro para escribir oyentes:

Un oyente debe ser rápido. Si tiene trabajo pesado, que lo delegue.

// MAL: trabajo pesado sincrono dentro del oyente.
gestor.on('sesion-agotada', (datos) => {
  generarInformePdfSincrono(datos);   // Bloquea a todo el mundo
});

// BIEN: el oyente solo encola el trabajo y devuelve el control.
gestor.on('sesion-agotada', (datos) => {
  setImmediate(() => generarInformePdf(datos));
});

// MEJOR: trabajo asincrono de verdad, con su propio manejo de errores.
gestor.on('sesion-agotada', (datos) => {
  enviarCorreoOrganizador(datos).catch((error) => {
    console.error(`[correo] fallo al avisar de ${datos.sesionId}: ${error.message}`);
  });
});

Fíjate en ese .catch del último ejemplo. Es obligatorio. Un oyente async cuya promesa se rechace produce un rechazo no manejado que, como aprendiste en la lección anterior, tumba el proceso. Y emit no puede ayudarte: devolvió el control mucho antes de que la promesa se asentara.

El orden importa

Como los oyentes se ejecutan en orden de registro, ese orden es parte del comportamiento observable:

emisor.on('venta', () => console.log('normal'));
emisor.prependListener('venta', () => console.log('primero, siempre'));
emisor.emit('venta');
// primero, siempre
// normal

Usar prependListener para "colarse" delante suele ser señal de un diseño frágil. Si el orden de los oyentes importa de verdad, probablemente lo que necesitas no es un evento sino una secuencia explícita de pasos.

  1. Argumentos en emit y el valor de this

4.1 Pasar datos

emit acepta cualquier número de argumentos tras el nombre del evento, y todos llegan a cada oyente:

emisor.emit('venta-registrada', 'ses-002-2', 3, 5400);

emisor.on('venta-registrada', (sesionId, cantidad, importeCentimos) => {
  console.log(`${cantidad} entradas de ${sesionId} por ${importeCentimos} centimos`);
});

Funciona, pero en Escena Viva usaremos siempre un único objeto:

emisor.emit('venta-registrada', {
  sesionId: 'ses-002-2',
  eventoId: 'evt-002',
  cantidad: 3,
  importeCentimos: 5400,
  libresRestantes: 72,
  fechaHora: '2026-08-11T18:42:11'
});

Las razones son las mismas que para cualquier API:

Argumentos sueltos Un objeto
El orden importa y es fácil equivocarse Los nombres se autodocumentan
Añadir un campo rompe a todos los oyentes Añadir un campo no rompe nada
Un oyente que solo quiere el tercer dato debe declarar los tres Desestructura solo lo que necesita
// El oyente toma solo lo que le interesa.
gestor.on('venta-registrada', ({ sesionId, libresRestantes }) => {
  console.log(`${sesionId}: quedan ${libresRestantes}`);
});

4.2 El valor de this

Dentro de un oyente registrado con function, this es el emisor:

// src/laboratorio/this-en-oyentes.js
const EventEmitter = require('node:events');
const emisor = new EventEmitter();

emisor.on('venta', function () {
  console.log('Con function, this es:', this.constructor.name);
  console.log('  eventos registrados:', this.eventNames());
});

emisor.on('venta', () => {
  // Una funcion flecha NO tiene this propio: hereda el del ambito donde se escribio.
  console.log('Con flecha, this es:', this);
});

emisor.emit('venta');
Con function, this es: EventEmitter
  eventos registrados: [ 'venta' ]
Con flecha, this es: {}

Node vincula this al emisor cuando el oyente es una función normal. Con una función flecha, this es el del ámbito circundante —en un módulo CommonJS de nivel superior, module.exports, que aparece como {}—.

function () {} () => {}
this El emisor El del ámbito donde se escribió
Acceso a this.eventNames(), this.off(...) Sí No
Dentro de un método de clase this deja de ser la instancia this sigue siendo la instancia

Ese último punto es el que decide en la práctica. Dentro de una clase, la flecha es casi siempre lo que quieres:

class PanelDeControl {
  constructor(gestor) {
    this.agotadas = [];

    // Con flecha: this sigue siendo el PanelDeControl. Correcto.
    gestor.on('sesion-agotada', (datos) => {
      this.agotadas.push(datos.sesionId);
    });

    // Con function: this seria el gestor, y this.agotadas seria undefined.
    // gestor.on('sesion-agotada', function (datos) {
    //   this.agotadas.push(datos.sesionId);   // TypeError
    // });
  }
}

Recomendación para el curso: usa funciones flecha en los oyentes. El acceso a this como emisor rara vez hace falta, y cuando lo necesitas tienes la referencia al emisor en una variable de todos modos.

  1. Heredar de EventEmitter: el GestorDeVentas de Escena Viva

Llegamos al ejemplo central de la lección y al compromiso que adquirimos en el Módulo 1. Vamos a crear una clase de dominio que es un emisor de eventos.

Crea src/dominio/gestor-ventas.js:

// src/dominio/gestor-ventas.js
// Registra las ventas de Escena Viva y avisa al resto del sistema
// mediante eventos, sin conocer a ninguno de sus consumidores.

const EventEmitter = require('node:events');

// Umbral por debajo del cual se considera que el aforo esta "bajo".
const UMBRAL_AFORO_BAJO = 0.10;   // 10 %

class GestorDeVentas extends EventEmitter {
  // Sesiones indexadas por id, para acceso directo.
  #sesiones = new Map();

  // Contador de codigos de entrada emitidos este ano.
  #secuenciaEntrada = 0;

  constructor(eventos = []) {
    // super() es OBLIGATORIO y debe ir antes de usar this:
    // inicializa la maquinaria interna de EventEmitter.
    super();

    for (const evento of eventos) {
      for (const sesion of evento.sesiones) {
        this.#sesiones.set(sesion.id, { ...sesion, eventoId: evento.id, titulo: evento.titulo });
      }
    }
  }

  get numeroSesiones() {
    return this.#sesiones.size;
  }

  obtenerSesion(sesionId) {
    return this.#sesiones.get(sesionId);
  }

  // Genera un codigo con el formato del proyecto: EV-2026-000123
  #generarCodigoEntrada() {
    this.#secuenciaEntrada++;
    const anio = new Date().getFullYear();
    return `EV-${anio}-${String(this.#secuenciaEntrada).padStart(6, '0')}`;
  }

  /**
   * Registra la venta de "cantidad" entradas de una sesion.
   * Emite: 'venta-registrada' siempre; ademas 'aforo-bajo' o 'sesion-agotada'
   * si la venta cruza esos umbrales.
   * Devuelve las entradas emitidas.
   */
  registrarVenta(sesionId, cantidad, pedidoId) {
    const sesion = this.#sesiones.get(sesionId);

    // Los errores de USO se lanzan: son fallos del programador, no sucesos.
    if (!sesion) {
      const error = new Error(`Sesion no encontrada: ${sesionId}`);
      error.codigo = 'SESION_NO_ENCONTRADA';
      throw error;
    }
    if (!Number.isInteger(cantidad) || cantidad < 1) {
      const error = new Error('La cantidad debe ser un entero positivo');
      error.codigo = 'CANTIDAD_INVALIDA';
      throw error;
    }

    const libresAntes = sesion.aforo - sesion.vendidas;

    if (cantidad > libresAntes) {
      const error = new Error(
        `Aforo insuficiente en ${sesionId}: pides ${cantidad} y quedan ${libresAntes}`
      );
      error.codigo = 'AFORO_INSUFICIENTE';
      throw error;
    }

    // El JavaScript de usuario es monohilo: esta actualizacion es atomica.
    sesion.vendidas += cantidad;
    const libresDespues = sesion.aforo - sesion.vendidas;

    const entradas = [];
    for (let i = 0; i < cantidad; i++) {
      entradas.push({
        codigo: this.#generarCodigoEntrada(),
        sesionId,
        pedidoId,
        estado: 'valida'
      });
    }

    // --- Anuncio de lo ocurrido ---

    this.emit('venta-registrada', {
      sesionId,
      eventoId: sesion.eventoId,
      titulo: sesion.titulo,
      cantidad,
      importeCentimos: cantidad * sesion.precioCentimos,
      libresRestantes: libresDespues,
      ocupacion: Math.round((sesion.vendidas / sesion.aforo) * 100)
    });

    const proporcionLibre = libresDespues / sesion.aforo;
    const proporcionLibreAntes = libresAntes / sesion.aforo;

    if (libresDespues === 0) {
      // Agotada: el evento que prometimos en el modulo 1.
      this.emit('sesion-agotada', {
        sesionId,
        eventoId: sesion.eventoId,
        titulo: sesion.titulo,
        aforo: sesion.aforo,
        recaudacionCentimos: sesion.vendidas * sesion.precioCentimos
      });
    } else if (proporcionLibre < UMBRAL_AFORO_BAJO && proporcionLibreAntes >= UMBRAL_AFORO_BAJO) {
      // Solo al CRUZAR el umbral, no en cada venta posterior.
      this.emit('aforo-bajo', {
        sesionId,
        eventoId: sesion.eventoId,
        titulo: sesion.titulo,
        libresRestantes: libresDespues,
        porcentajeLibre: Math.round(proporcionLibre * 100)
      });
    }

    return entradas;
  }
}

module.exports = { GestorDeVentas, UMBRAL_AFORO_BAJO };

Esa última línea, module.exports, es lo que convierte el fichero en un módulo reutilizable. La estudiaremos a fondo en la lección Módulos CommonJS y require(); de momento acepta que expone la clase para que otros ficheros puedan usarla con require.

Ahora los consumidores. Crea src/laboratorio/venta-con-eventos.js:

// src/laboratorio/venta-con-eventos.js
const { GestorDeVentas } = require('../dominio/gestor-ventas.js');

const catalogo = [
  {
    id: 'evt-002',
    titulo: 'Noche de Monologos',
    sesiones: [
      { id: 'ses-002-1', fechaHora: '2026-10-10T21:30:00', aforo: 120, vendidas: 100, precioCentimos: 1800 },
      { id: 'ses-002-2', fechaHora: '2026-10-11T21:30:00', aforo: 120, vendidas: 45,  precioCentimos: 1800 }
    ]
  }
];

const gestor = new GestorDeVentas(catalogo);

// --- Oyentes: cada uno con una unica responsabilidad ---

// 1. Registro de operacion, por stderr.
gestor.on('venta-registrada', ({ sesionId, cantidad, importeCentimos, ocupacion }) => {
  console.error(
    `[venta] ${cantidad} entradas de ${sesionId} por ` +
    `${(importeCentimos / 100).toFixed(2)} EUR (${ocupacion}% ocupado)`
  );
});

// 2. Aviso comercial al organizador.
gestor.on('aforo-bajo', ({ titulo, sesionId, libresRestantes, porcentajeLibre }) => {
  console.error(
    `[aviso] "${titulo}" (${sesionId}) al ${100 - porcentajeLibre}%: ` +
    `solo quedan ${libresRestantes} entradas`
  );
});

// 3. Sesion agotada: varios oyentes independientes para el MISMO evento.
gestor.on('sesion-agotada', ({ titulo, sesionId, recaudacionCentimos }) => {
  console.log(
    `AGOTADA: "${titulo}" (${sesionId}). ` +
    `Recaudacion: ${(recaudacionCentimos / 100).toFixed(2)} EUR`
  );
});

gestor.on('sesion-agotada', ({ sesionId }) => {
  console.error(`[catalogo] marcando ${sesionId} como no disponible`);
});

gestor.on('sesion-agotada', ({ sesionId }) => {
  // Trabajo pesado: SIEMPRE delegado, nunca sincrono dentro del oyente.
  setImmediate(() => {
    console.error(`[correo] aviso de agotamiento enviado para ${sesionId}`);
  });
});

// --- Simulacion de la apertura de venta ---

console.error(`Gestor con ${gestor.numeroSesiones} sesiones. Empieza la venta.`);
console.error('');

gestor.registrarVenta('ses-002-1', 5, 'ped-001');    // 105/120: 12,5% libre
gestor.registrarVenta('ses-002-1', 4, 'ped-002');    // 109/120: 9,2% libre -> aforo-bajo
gestor.registrarVenta('ses-002-1', 6, 'ped-003');    // 115/120: sigue bajo, NO reemite
gestor.registrarVenta('ses-002-1', 5, 'ped-004');    // 120/120 -> sesion-agotada

// Un error de uso: se lanza, no se emite.
try {
  gestor.registrarVenta('ses-002-1', 1, 'ped-005');
} catch (error) {
  console.error(`[error] [${error.codigo}] ${error.message}`);
}
Gestor con 2 sesiones. Empieza la venta.

[venta] 5 entradas de ses-002-1 por 90.00 EUR (88% ocupado)
[venta] 4 entradas de ses-002-1 por 72.00 EUR (91% ocupado)
[aviso] "Noche de Monologos" (ses-002-1) al 91%: solo quedan 11 entradas
[venta] 6 entradas de ses-002-1 por 108.00 EUR (96% ocupado)
[venta] 5 entradas de ses-002-1 por 90.00 EUR (100% ocupado)
AGOTADA: "Noche de Monologos" (ses-002-1). Recaudacion: 2160.00 EUR
[catalogo] marcando ses-002-1 como no disponible
[error] [AFORO_INSUFICIENTE] Aforo insuficiente en ses-002-1: pides 1 y quedan 0
[correo] aviso de agotamiento enviado para ses-002-1

Cinco decisiones de diseño que merece la pena señalar:

  1. super() es obligatorio y debe ir antes de cualquier uso de this. Sin él, la maquinaria interna de EventEmitter no se inicializa y emit falla.
  2. Nombres de evento en kebab-case: venta-registrada, aforo-bajo, sesion-agotada. Es la convención del ecosistema y evita problemas al usarlos como cadenas.
  3. aforo-bajo solo se emite al cruzar el umbral. Si se emitiera en cada venta por debajo del 10 %, el organizador recibiría veinte correos. Detectar la transición en vez del estado es un detalle que separa un aviso útil de una molestia.
  4. Los errores de uso se lanzan, no se emiten. Un aforo insuficiente es un error del código que llama; una sesión agotada es un suceso del dominio. Confundirlos es el fallo de diseño más común con EventEmitter.
  5. Tres oyentes distintos para sesion-agotada, cada uno con una responsabilidad. El gestor no conoce a ninguno, y añadir un cuarto no requiere tocarlo.

Fíjate también en el orden de la salida: el [correo] aparece al final, después incluso del error. Es el setImmediate haciendo su trabajo: cede el control al bucle de eventos y se ejecuta en la fase check, cuando todo lo síncrono ha terminado. Exactamente lo que aprendiste en la lección del bucle de eventos.

  1. El evento error: el único que puede matarte

EventEmitter trata un nombre de evento de forma especial: error.

Si se emite 'error' y no hay ningún oyente registrado, EventEmitter lanza el error. Si nadie lo captura, el proceso muere.

// src/laboratorio/evento-error.js
const EventEmitter = require('node:events');
const emisor = new EventEmitter();

emisor.emit('evento-cualquiera');   // Sin oyentes: no pasa nada, devuelve false

emisor.emit('error', new Error('La pasarela de pago no responde'));
// El proceso MUERE aqui.

console.log('Esto nunca se imprime');
node:events:496
      throw er; // Unhandled 'error' event
      ^
Error: La pasarela de pago no responde
    at Object.<anonymous> ...
Emitted 'error' event on EventEmitter instance at:
    ...
[el proceso muere con codigo 1]

¿Por qué este diseño tan drástico? Por la misma filosofía que hace que los rechazos no manejados tumben el proceso: un error que nadie mira es peor que un proceso caído. Un servidor que sigue en pie ignorando errores de conexión acumula estado corrupto y falla de formas incomprensibles horas después. Node prefiere fallar ruidosamente.

La solución es trivial, y es obligatoria en cualquier emisor que pueda fallar:

emisor.on('error', (error) => {
  console.error(`[gestor] error: ${error.message}`);
  // Aqui: registrar, avisar, decidir. Pero NO ignorar.
});

emisor.emit('error', new Error('La pasarela de pago no responde'));
// Ahora se trata correctamente y el proceso continua.

Aplicado al GestorDeVentas, para fallos asíncronos que no pueden lanzarse:

// Dentro de la clase: un fallo en un proceso de fondo se anuncia como evento.
sincronizarConPasarela()
  .catch((error) => {
    error.codigo = error.codigo || 'SINCRONIZACION_FALLIDA';
    this.emit('error', error);
  });

Y en el consumidor, sin excepción:

const gestor = new GestorDeVentas(catalogo);

// SIEMPRE, antes de nada.
gestor.on('error', (error) => {
  console.error(`[gestor] [${error.codigo || 'ERROR'}] ${error.message}`);
  process.exitCode = 1;
});

Cuándo lanzar y cuándo emitir error:

Situación Qué hacer
Argumento inválido, precondición incumplida (fallo del programador) throw
Fallo asíncrono en un proceso en marcha (red caída, disco lleno) emit('error', ...)
Suceso de negocio esperado (aforo agotado) emit('sesion-agotada', ...), no es un error

En la lección de Manejo de Errores del Módulo 6 formalizaremos esta distinción entre errores de programación y errores operativos.

  1. Fugas de memoria por oyentes

Un oyente registrado es una referencia viva: mantiene en memoria la función y todo lo que su cierre captura. Si registras oyentes y nunca los quitas, la memoria crece sin parar. Es la fuga de memoria más común en aplicaciones Node.

Node te avisa cuando huele mal:

// src/laboratorio/fuga-oyentes.js
const EventEmitter = require('node:events');
const gestor = new EventEmitter();

// Simulamos una peticion HTTP que registra un oyente y se olvida de quitarlo.
function atenderPeticion(numero) {
  const datosDeLaPeticion = new Array(1000).fill(`peticion-${numero}`);

  gestor.on('sesion-agotada', () => {
    // El cierre retiene datosDeLaPeticion: 1000 elementos por peticion.
    console.log(`Peticion ${numero} enterada (${datosDeLaPeticion.length} datos retenidos)`);
  });
}

for (let i = 1; i <= 12; i++) {
  atenderPeticion(i);
}
(node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak
detected. 11 sesion-agotada listeners added to [EventEmitter]. Use
emitter.setMaxListeners() to increase limit

El aviso salta al undécimo oyente del mismo evento. Es solo un aviso: el programa sigue funcionando y los oyentes se siguen añadiendo. Pero es una señal casi siempre correcta de que algo se está registrando y nunca se quita.

Las tres respuestas posibles

1. El aviso tiene razón: hay una fuga. Desregistra.

function atenderPeticion(numero) {
  const alAgotarse = (datos) => {
    console.log(`Peticion ${numero}: ${datos.sesionId} agotada`);
  };

  gestor.on('sesion-agotada', alAgotarse);

  // Al terminar la peticion, quitamos el oyente.
  return function terminar() {
    gestor.off('sesion-agotada', alAgotarse);
  };
}

const terminar = atenderPeticion(1);
// ... se atiende la peticion ...
terminar();

2. El suceso ocurre una sola vez: usa once.

// once se desregistra solo. Sin fuga posible.
gestor.once('sesion-agotada', (datos) => {
  console.log(`Primera sesion agotada del dia: ${datos.sesionId}`);
});

3. Realmente necesitas más de diez oyentes: sube el límite conscientemente.

// Solo si sabes que 50 oyentes es el diseno correcto, no un parche.
gestor.setMaxListeners(50);

// O globalmente, para todos los emisores (raramente justificado):
// require('node:events').defaultMaxListeners = 20;

Nunca subas el límite para "quitar el aviso". El aviso existe para avisarte. Si lo silencias sin entender por qué salta, has cambiado un mensaje molesto por una fuga silenciosa que tumbará el proceso en producción a las tres de la mañana.

Diagnóstico

// Herramientas de inspeccion, utiles cuando el aviso salta.
console.log(gestor.eventNames());
// [ 'sesion-agotada', 'venta-registrada', 'error' ]

console.log(gestor.listenerCount('sesion-agotada'));
// 12

console.log(gestor.listeners('sesion-agotada').map((f) => f.name));
// [ 'alAgotarse', 'alAgotarse', ... ]  <- 12 veces la misma: sospechoso

Si en un servidor ves que listenerCount crece con el número de peticiones atendidas, tienes una fuga confirmada. Es exactamente el tipo de problema que se detecta con la tendencia de heapUsed de la primera lección del módulo.

  1. events.once(): esperar un evento con await

A veces solo quieres esperar a que algo ocurra una vez. Con la API de oyentes eso obliga a envolverlo todo en un callback. El módulo events ofrece un puente hacia las promesas:

// src/laboratorio/esperar-evento.js
const { once } = require('node:events');
const { GestorDeVentas } = require('../dominio/gestor-ventas.js');

const gestor = new GestorDeVentas(catalogo);

async function esperarPrimerAgotamiento() {
  console.log('Esperando a que se agote la primera sesion...');

  // once() devuelve una PROMESA que se cumple con un ARRAY:
  // los argumentos con los que se emitio el evento.
  const [datos] = await once(gestor, 'sesion-agotada');

  console.log(`Agotada: ${datos.titulo} (${datos.sesionId})`);
  return datos;
}

esperarPrimerAgotamiento().catch((error) => {
  console.error(`Fallo: ${error.message}`);
  process.exitCode = 1;
});

// Provocamos el agotamiento un poco despues.
setTimeout(() => {
  gestor.registrarVenta('ses-002-1', 20, 'ped-100');
}, 300);
Esperando a que se agote la primera sesion...
Agotada: Noche de Monologos (ses-002-1)

Detalles importantes de events.once():

  • Devuelve un array, porque emit puede pasar varios argumentos. Por eso se desestructura con const [datos] = ....
  • Rechaza automáticamente si el emisor emite 'error' mientras espera. Ese comportamiento es muy conveniente: no tienes que gestionar dos casos a mano.
  • Acepta una señal de cancelación para no esperar indefinidamente:
const controlador = new AbortController();
setTimeout(() => controlador.abort(), 5000);

try {
  const [datos] = await once(gestor, 'sesion-agotada', { signal: controlador.signal });
  console.log(`Agotada: ${datos.sesionId}`);
} catch (error) {
  if (error.name === 'AbortError') {
    console.error('Ninguna sesion se agoto en 5 segundos');
  } else {
    throw error;
  }
}

Y su compañero events.on(), que devuelve un iterador asíncrono para consumir un flujo de eventos con for await:

const { on } = require('node:events');

async function seguirVentas(gestor) {
  // Consume TODAS las ventas segun van ocurriendo.
  for await (const [datos] of on(gestor, 'venta-registrada')) {
    console.log(`${datos.cantidad} entradas de ${datos.sesionId}`);
  }
}

Cuidado con for await: este bucle no termina nunca por sí solo. Necesita una señal de cancelación o un break. Y mientras procesas un evento, los que lleguen se acumulan en un búfer interno; si tu procesamiento es más lento que la emisión, ese búfer crece sin límite.

  1. Eventos, callbacks o promesas: tabla de decisión

Ya tienes las tres herramientas. Elegir bien es una decisión de diseño, no de gusto.

Criterio Callback Promesa / async-await EventEmitter
Número de resultados Uno (por llamada) Exactamente uno Muchos, a lo largo del tiempo
Número de interesados Uno Muchos (varios .then) Muchos, y desconocidos
Quién decide qué pasa después Quien llama Quien llama Cada oyente, por su cuenta
Acoplamiento Directo Directo Nulo
Ejecución Asíncrona Asíncrona (microtareas) Síncrona
Manejo de errores (error, ...) try/catch Evento error (¡o muere!)
¿Se puede esperar con await? Con promisify Nativamente Con events.once()
Se puede llegar tarde No Sí (la promesa guarda el resultado) No (te pierdes lo emitido)

Y la guía práctica:

Si tu caso es… Usa
"Dame el evento evt-002" Promesa
"Cobra este pedido y dime si funcionó" Promesa
"Avísame cada vez que llegue un chunk de datos" EventEmitter (streams)
"Avísame cuando una sesión se agote, sea cuando sea" EventEmitter
"Quiero que varias partes del sistema reaccionen sin conocerse" EventEmitter
"La API de Node solo acepta callback" Callback (o promisify)
"Necesito reaccionar a lo que ocurra, una única vez" events.once() + await

La regla de una línea:

Una respuesta → promesa. Muchas notificaciones → evento.

Y un antipatrón a evitar: no uses eventos para el flujo principal de una operación. Emitir 'pedido-creado' y esperar que un oyente continúe el flujo de compra convierte un proceso lineal y depurable en una cadena invisible de saltos. Los eventos son para efectos secundarios desacoplados —notificar, registrar, medir—, no para partir en trozos algo que debería leerse de arriba abajo.

  1. Todo Node es un EventEmitter

Cuando entiendes EventEmitter desbloqueas media biblioteca estándar, porque casi todo hereda de ella:

// El objeto process: un EventEmitter.
process.on('exit', (codigo) => console.error(`Saliendo con codigo ${codigo}`));
process.on('SIGINT', () => {
  console.error('Ctrl+C recibido: cerrando ordenadamente...');
  process.exit(0);
});
process.on('unhandledRejection', (motivo) => console.error(motivo));
// Un servidor HTTP: un EventEmitter. (Modulo 4)
const http = require('node:http');
const servidor = http.createServer();

servidor.on('request', (peticion, respuesta) => {
  respuesta.end('Escena Viva');
});
servidor.on('listening', () => console.error('Servidor listo'));
servidor.on('close', () => console.error('Servidor cerrado'));
servidor.on('error', (error) => console.error(error.message));
// Un stream de lectura: un EventEmitter. (Modulo 3)
const fs = require('node:fs');
const lector = fs.createReadStream('datos/ventas.csv');

lector.on('data', (trozo) => console.error(`Recibidos ${trozo.length} bytes`));
lector.on('end', () => console.error('Fichero completo'));
lector.on('error', (error) => console.error(`Error de lectura: ${error.message}`));
Objeto de Node Eventos habituales Módulo del curso
process exit, SIGINT, uncaughtException, unhandledRejection 2 y 11
Servidor HTTP request, listening, close, error 4
Streams data, end, error, close, finish 3
Sockets TCP connect, data, end, error 4
Procesos hijo exit, close, message, error 10
fs.watch change, rename 3

Esto significa que lo aprendido aquí no es específico de GestorDeVentas: es el vocabulario con el que Node habla contigo en todos los módulos que quedan. Cuando en el Módulo 3 leas lector.on('data', ...), ya sabrás que es un oyente síncrono en una lista, que el orden de registro importa, que un oyente lento bloquea el proceso y que el evento error hay que registrarlo siempre.

Errores Comunes y Consejos

Error 1: no registrar un oyente para error. El único evento que mata el proceso si nadie lo escucha. En todo emisor que pueda fallar: emisor.on('error', ...).

Error 2: creer que emit es asíncrono. Es completamente síncrono. Un oyente lento bloquea el hilo principal y a todos los demás oyentes.

Error 3: hacer trabajo pesado dentro de un oyente. Delega con setImmediate o con una operación asíncrona real, y no olvides el .catch.

Error 4: un oyente async sin .catch. emit no espera a la promesa y no captura su rechazo. Un rechazo no manejado tumba el proceso.

Error 5: intentar quitar un oyente con una función distinta. off compara por referencia. Guarda la función en una variable si vas a desregistrarla.

Error 6: olvidar super() al heredar de EventEmitter. ReferenceError: Must call super constructor o, peor, un emisor a medio inicializar.

Error 7: subir setMaxListeners para silenciar el aviso. Cambia un mensaje incómodo por una fuga silenciosa. Investiga primero por qué se acumulan.

Error 8: emitir un evento en el constructor. Nadie ha podido suscribirse todavía. Si tienes que hacerlo, aplázalo con process.nextTick, como viste en la lección del bucle de eventos.

Error 9: usar eventos para el flujo principal. Convierte código lineal en saltos invisibles imposibles de seguir. Los eventos son para efectos secundarios desacoplados.

Consejo 1: nombres de evento en kebab-case y en pasado. venta-registrada, sesion-agotada: describen algo que ya ha ocurrido, que es la semántica correcta de un evento.

Consejo 2: emite siempre un único objeto. Añadir campos no rompe a nadie y los oyentes desestructuran lo que necesitan.

Consejo 3: emite en las transiciones, no en los estados. aforo-bajo solo al cruzar el umbral, nunca en cada venta posterior.

Consejo 4: documenta los eventos de tus clases. Los eventos son parte de la API pública, tanto como los métodos. Si no están documentados, nadie los usará.

Ejercicios

Ejercicio 1: un panel de control como oyente

Escribe src/laboratorio/panel-ventas.js con una clase PanelDeVentas que no herede de EventEmitter sino que se suscriba a un GestorDeVentas, y que mantenga:

  1. Total de entradas vendidas y recaudación acumulada en céntimos.
  2. Lista de sesiones agotadas y lista de sesiones en aviso de aforo bajo.
  3. Un método resumen() que devuelva un objeto con esos datos y la recaudación formateada en euros.
  4. Un método desconectar() que desregistre todos sus oyentes del gestor.

Requisitos técnicos:

  • Los oyentes deben ser funciones flecha o métodos vinculados, de forma que this siga siendo el panel.
  • desconectar() debe dejar gestor.listenerCount() en el mismo valor que tenía antes de conectar el panel. Demuéstralo imprimiendo los contadores antes, durante y después.
  • Simula una tanda de ventas que agote una sesión y verifica el resumen.

Ejercicio 2: el evento error y el aviso de fuga

Escribe src/laboratorio/emisor-robusto.js que demuestre, en un único programa y sin que el proceso muera:

  1. Que emitir 'error' sin oyente lanzaría una excepción. Captúrala con try/catch alrededor del emit y explica en un comentario por qué ahí sí funciona el try/catch (pista: emit es síncrono).
  2. Que con un oyente de 'error' registrado el proceso continúa normalmente.
  3. Que registrar 11 oyentes del mismo evento produce MaxListenersExceededWarning. Captúralo con process.on('warning', ...) e imprime el nombre del aviso en lugar de dejar que salga por defecto.
  4. Que tras setMaxListeners(20) ya no aparece el aviso.
  5. Un pequeño diagnóstico final con eventNames() y listenerCount() de cada evento.

Ejercicio 3: esperar el agotamiento con await y tiempo límite

Escribe src/laboratorio/esperar-agotamiento.js que:

  1. Cree un GestorDeVentas con las tres sesiones de evt-002 (aforos 120, ventas iniciales 118, 45 y 12).
  2. Implemente esperarAgotamiento(gestor, limiteMs) usando events.once() con un AbortController para no esperar más de limiteMs.
  3. Simule ventas aleatorias cada 100 ms sobre sesiones al azar, con cantidades de 1 a 5, hasta que algo se agote o venza el límite.
  4. Informe del resultado: qué sesión se agotó y en cuántos milisegundos, o que venció el tiempo límite.
  5. Trate correctamente el AbortError (mensaje claro, process.exitCode = 1) y cualquier otro error (relanzarlo).
  6. Limpie los temporizadores al terminar para que el proceso salga solo.

Responde además: ¿por qué events.once() es mejor aquí que registrar un once con callback? ¿Y qué pasaría si la sesión se agotara antes de llamar a esperarAgotamiento?

Soluciones

Solución 1

// src/laboratorio/panel-ventas.js
// Un consumidor de eventos que no hereda de EventEmitter: solo escucha.

const { GestorDeVentas } = require('../dominio/gestor-ventas.js');

class PanelDeVentas {
  #gestor;
  #conectado = false;

  constructor(gestor) {
    this.#gestor = gestor;

    this.entradasVendidas = 0;
    this.recaudacionCentimos = 0;
    this.agotadas = [];
    this.enAviso = [];

    // Guardamos las referencias: son necesarias para poder desregistrar.
    // Las flechas conservan "this" apuntando al panel.
    this.alVenderse = ({ cantidad, importeCentimos }) => {
      this.entradasVendidas += cantidad;
      this.recaudacionCentimos += importeCentimos;
    };

    this.alAgotarse = ({ sesionId, titulo }) => {
      this.agotadas.push({ sesionId, titulo });
    };

    this.alBajarAforo = ({ sesionId, libresRestantes }) => {
      this.enAviso.push({ sesionId, libresRestantes });
    };

    this.conectar();
  }

  conectar() {
    if (this.#conectado) return;
    this.#gestor.on('venta-registrada', this.alVenderse);
    this.#gestor.on('sesion-agotada', this.alAgotarse);
    this.#gestor.on('aforo-bajo', this.alBajarAforo);
    this.#conectado = true;
  }

  desconectar() {
    if (!this.#conectado) return;
    this.#gestor.off('venta-registrada', this.alVenderse);
    this.#gestor.off('sesion-agotada', this.alAgotarse);
    this.#gestor.off('aforo-bajo', this.alBajarAforo);
    this.#conectado = false;
  }

  resumen() {
    return {
      entradasVendidas: this.entradasVendidas,
      recaudacionEuros: (this.recaudacionCentimos / 100).toFixed(2),
      sesionesAgotadas: this.agotadas.length,
      sesionesEnAviso: this.enAviso.length,
      agotadas: this.agotadas.map((a) => a.sesionId)
    };
  }
}

// --- Demostracion ---

const catalogo = [
  {
    id: 'evt-002',
    titulo: 'Noche de Monologos',
    sesiones: [
      { id: 'ses-002-1', fechaHora: '2026-10-10T21:30:00', aforo: 120, vendidas: 100, precioCentimos: 1800 },
      { id: 'ses-002-2', fechaHora: '2026-10-11T21:30:00', aforo: 120, vendidas: 45,  precioCentimos: 1800 }
    ]
  }
];

const gestor = new GestorDeVentas(catalogo);

function contadores(etiqueta) {
  console.error(
    `${etiqueta.padEnd(24)} venta-registrada=${gestor.listenerCount('venta-registrada')} ` +
    `sesion-agotada=${gestor.listenerCount('sesion-agotada')} ` +
    `aforo-bajo=${gestor.listenerCount('aforo-bajo')}`
  );
}

contadores('Antes de conectar:');
const panel = new PanelDeVentas(gestor);
contadores('Con el panel conectado:');

gestor.registrarVenta('ses-002-1', 8, 'ped-001');    // 108/120 -> aforo-bajo
gestor.registrarVenta('ses-002-1', 12, 'ped-002');   // 120/120 -> agotada
gestor.registrarVenta('ses-002-2', 10, 'ped-003');

console.log('');
console.log('RESUMEN DEL PANEL');
console.table([panel.resumen()]);

panel.desconectar();
contadores('Tras desconectar:');

// Una venta mas: el panel ya no debe enterarse.
gestor.registrarVenta('ses-002-2', 5, 'ped-004');
console.log('');
console.log(`Entradas tras desconectar (no debe cambiar): ${panel.resumen().entradasVendidas}`);
Antes de conectar:       venta-registrada=0 sesion-agotada=0 aforo-bajo=0
Con el panel conectado:  venta-registrada=1 sesion-agotada=1 aforo-bajo=1

RESUMEN DEL PANEL
┌─────────┬──────────────────┬──────────────────┬──────────────────┬─────────────────┬───────────────┐
│ (index) │ entradasVendidas │ recaudacionEuros │ sesionesAgotadas │ sesionesEnAviso │ agotadas      │
├─────────┼──────────────────┼──────────────────┼──────────────────┼─────────────────┼───────────────┤
│ 0       │ 30               │ '540.00'         │ 1                │ 1               │ ['ses-002-1'] │
└─────────┴──────────────────┴──────────────────┴──────────────────┴─────────────────┴───────────────┘

Tras desconectar:        venta-registrada=0 sesion-agotada=0 aforo-bajo=0

Entradas tras desconectar (no debe cambiar): 30

La clave está en guardar las referencias de las funciones en propiedades del panel. Si los oyentes se hubieran registrado como flechas anónimas dentro de conectar(), desconectar() no tendría con qué llamar a off y los contadores no volverían a cero. Esta disciplina es exactamente la que evita las fugas de memoria del apartado 7.

Solución 2

// src/laboratorio/emisor-robusto.js
// Demuestra el comportamiento del evento 'error' y del aviso de fuga de oyentes.

const EventEmitter = require('node:events');

// Interceptamos los avisos de Node para tratarlos nosotros (punto 3).
process.on('warning', (aviso) => {
  console.error(`[aviso capturado] ${aviso.name}: ${aviso.message.split('.')[0]}.`);
});

// --- 1. Emitir 'error' sin oyente ---
console.log('1. Emitir "error" sin oyente registrado');

const emisorSinOyente = new EventEmitter();

// El try/catch SI funciona aqui porque emit es SINCRONO: la excepcion se
// lanza dentro de la misma pila de llamadas en la que estamos.
try {
  emisorSinOyente.emit('error', new Error('La pasarela de pago no responde'));
  console.log('   Esto no se imprime.');
} catch (error) {
  console.log(`   Capturado: ${error.message}`);
  console.log('   Sin este try/catch, el proceso habria muerto.');
}

// --- 2. Con oyente registrado ---
console.log('');
console.log('2. Emitir "error" CON oyente registrado');

const emisorConOyente = new EventEmitter();
emisorConOyente.on('error', (error) => {
  console.log(`   Oyente de error: ${error.message}`);
});

emisorConOyente.emit('error', new Error('Fallo transitorio de red'));
console.log('   El proceso continua con normalidad.');

// --- 3. Aviso de fuga de oyentes ---
console.log('');
console.log('3. Registrar 11 oyentes del mismo evento');

const emisorConFuga = new EventEmitter();
for (let i = 1; i <= 11; i++) {
  emisorConFuga.on('sesion-agotada', () => {});
}
console.log(`   Oyentes registrados: ${emisorConFuga.listenerCount('sesion-agotada')}`);

// --- 4. Con el limite subido ---
console.log('');
console.log('4. Con setMaxListeners(20), sin aviso');

const emisorAmplio = new EventEmitter();
emisorAmplio.setMaxListeners(20);
for (let i = 1; i <= 15; i++) {
  emisorAmplio.on('sesion-agotada', () => {});
}
console.log(`   Oyentes registrados: ${emisorAmplio.listenerCount('sesion-agotada')} (sin aviso)`);

// --- 5. Diagnostico ---
console.log('');
console.log('5. Diagnostico de emisorConOyente');

emisorConOyente.on('venta-registrada', () => {});
emisorConOyente.on('venta-registrada', () => {});
emisorConOyente.on('aforo-bajo', () => {});

for (const nombre of emisorConOyente.eventNames()) {
  console.log(`   ${String(nombre).padEnd(20)} ${emisorConOyente.listenerCount(nombre)} oyente(s)`);
}
1. Emitir "error" sin oyente registrado
   Capturado: La pasarela de pago no responde
   Sin este try/catch, el proceso habria muerto.

2. Emitir "error" CON oyente registrado
   Oyente de error: Fallo transitorio de red
   El proceso continua con normalidad.

3. Registrar 11 oyentes del mismo evento
[aviso capturado] MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
   Oyentes registrados: 11

4. Con setMaxListeners(20), sin aviso
   Oyentes registrados: 15 (sin aviso)

5. Diagnostico de emisorConOyente
   error                1 oyente(s)
   venta-registrada     2 oyente(s)
   aforo-bajo           1 oyente(s)

La respuesta al punto 1 es la más instructiva de todo el ejercicio: try/catch funciona alrededor de un emit precisamente porque emit es síncrono. Es la excepción a la regla de la lección de callbacks. Si EventEmitter despachara de forma asíncrona, ese try/catch sería tan inútil como el que rodeaba un setTimeout.

Solución 3

// src/laboratorio/esperar-agotamiento.js
// Espera el primer agotamiento con await y un tiempo limite.

const { once } = require('node:events');
const { GestorDeVentas } = require('../dominio/gestor-ventas.js');

const catalogo = [
  {
    id: 'evt-002',
    titulo: 'Noche de Monologos',
    sesiones: [
      { id: 'ses-002-1', fechaHora: '2026-10-10T21:30:00', aforo: 120, vendidas: 118, precioCentimos: 1800 },
      { id: 'ses-002-2', fechaHora: '2026-10-11T21:30:00', aforo: 120, vendidas: 45,  precioCentimos: 1800 },
      { id: 'ses-002-3', fechaHora: '2026-10-17T21:30:00', aforo: 120, vendidas: 12,  precioCentimos: 1500 }
    ]
  }
];

const SESIONES = ['ses-002-1', 'ses-002-2', 'ses-002-3'];

// 2. Espera el evento con tiempo limite mediante AbortController.
async function esperarAgotamiento(gestor, limiteMs) {
  const controlador = new AbortController();
  const temporizador = setTimeout(() => controlador.abort(), limiteMs);

  try {
    const [datos] = await once(gestor, 'sesion-agotada', { signal: controlador.signal });
    return datos;
  } finally {
    // Sin este clearTimeout el temporizador mantendria vivo el proceso.
    clearTimeout(temporizador);
  }
}

async function principal() {
  const gestor = new GestorDeVentas(catalogo);

  // Obligatorio: sin oyente de 'error' un fallo mataria el proceso.
  gestor.on('error', (error) => {
    console.error(`[gestor] ${error.message}`);
  });

  gestor.on('aforo-bajo', ({ sesionId, libresRestantes }) => {
    console.error(`[aviso] ${sesionId}: quedan ${libresRestantes} entradas`);
  });

  const inicio = Date.now();

  // 3. Ventas aleatorias cada 100 ms.
  const simulador = setInterval(() => {
    const sesionId = SESIONES[Math.floor(Math.random() * SESIONES.length)];
    const cantidad = 1 + Math.floor(Math.random() * 5);

    try {
      gestor.registrarVenta(sesionId, cantidad, `ped-${Date.now()}`);
      console.error(`[venta] ${cantidad} entradas de ${sesionId}`);
    } catch (error) {
      // Aforo insuficiente es normal en una simulacion: se ignora.
      if (error.codigo !== 'AFORO_INSUFICIENTE') throw error;
    }
  }, 100);

  try {
    const datos = await esperarAgotamiento(gestor, 5000);

    // 4. Resultado.
    console.log('');
    console.log(`AGOTADA: "${datos.titulo}" (${datos.sesionId})`);
    console.log(`  Aforo       : ${datos.aforo}`);
    console.log(`  Recaudacion : ${(datos.recaudacionCentimos / 100).toFixed(2)} EUR`);
    console.log(`  Tiempo      : ${Date.now() - inicio} ms`);
  } catch (error) {
    // 5. Tratamiento diferenciado del AbortError.
    if (error.name === 'AbortError') {
      console.error('');
      console.error('Ninguna sesion se agoto en 5 segundos. Se agoto el tiempo limite.');
      process.exitCode = 1;
      return;
    }
    throw error;
  } finally {
    // 6. Limpieza: sin esto el setInterval mantiene vivo el proceso.
    clearInterval(simulador);
  }
}

principal().catch((error) => {
  console.error(`Fallo fatal: ${error.message}`);
  process.exitCode = 1;
});
[venta] 3 entradas de ses-002-3
[venta] 2 entradas de ses-002-2
[aviso] ses-002-1: quedan 2 entradas
[venta] 0 entradas de ses-002-1
[venta] 4 entradas de ses-002-3
[venta] 2 entradas de ses-002-1

AGOTADA: "Noche de Monologos" (ses-002-1)
  Aforo       : 120
  Recaudacion : 2160.00 EUR
  Tiempo      : 612 ms

Respuestas a las preguntas:

  • Por qué events.once() es mejor aquí: porque el flujo es "espera un resultado y continúa", que es exactamente la forma de una promesa. Con un gestor.once('sesion-agotada', callback) habría que meter todo el código posterior dentro del callback, perder try/catch y gestionar el tiempo límite a mano con un setTimeout que además tendría que desregistrar el oyente. events.once() con AbortSignal resuelve las cuatro cosas de una vez, y rechaza automáticamente si el gestor emite error.
  • Si la sesión se agotara antes de llamar a esperarAgotamiento, el evento se habría perdido para siempre: esperarAgotamiento se quedaría esperando hasta agotar su límite de 5 segundos. Un evento no tiene memoria, a diferencia de una promesa, que guarda su resultado para quien se suscriba después. Es la diferencia más importante entre ambos mecanismos y la razón por la que los emisores nunca deben emitir en su constructor: hay que dar tiempo a suscribirse.

Conclusión

Has incorporado la tercera pieza de la asincronía en Node. El patrón observador permite que un objeto anuncie que algo ha ocurrido sin conocer a quienes reaccionan, y EventEmitter es su implementación en el núcleo de Node: on para suscribirse, once para una sola vez, emit para anunciar, off para desregistrar, y listenerCount/eventNames para diagnosticar.

Has descubierto el hecho que más sorprende y que más consecuencias tiene: los oyentes se ejecutan de forma síncrona, en orden de registro, y emit no devuelve hasta que todos han terminado. De ahí la regla de que un oyente debe ser rápido y delegar el trabajo pesado con setImmediate o con una operación asíncrona que siempre lleve su .catch. Y de ahí también la única excepción a lo aprendido en la lección de callbacks: alrededor de un emit, try/catch sí funciona.

Has construido el compromiso del Módulo 1: src/dominio/gestor-ventas.js, con la clase GestorDeVentas extends EventEmitter que emite venta-registrada en cada operación, aforo-bajo al cruzar el umbral del 10 % libre —no en cada venta posterior— y sesion-agotada cuando ya no quedan entradas, con tres oyentes independientes suscritos al mismo evento y ninguno conocido por el gestor. Y has fijado la distinción de diseño que importa: los errores de uso se lanzan, los sucesos del dominio se emiten, y los fallos asíncronos van al evento error.

Sabes que error es el único evento que mata el proceso si nadie lo escucha, y por qué Node prefiere fallar ruidosamente a acumular estado corrupto. Sabes que los oyentes son referencias vivas y que un MaxListenersExceededWarning casi siempre señala una fuga real —nunca se silencia subiendo el límite sin investigar—. Y tienes el puente con la lección anterior: events.once() devuelve una promesa que puedes esperar con await, con AbortSignal para el tiempo límite y rechazo automático ante un error. La regla que resume todo: una respuesta → promesa; muchas notificaciones → evento.

Por último, has visto que esto no es un rincón del lenguaje: process, el servidor HTTP, los sockets, los streams y los procesos hijo son todos EventEmitter. Lo aprendido aquí es el vocabulario de los diez módulos restantes.

Y aun así, hay algo que llevamos arrastrando desde el principio. En cada fichero de laboratorio has vuelto a copiar el array catalogo. Has escrito module.exports tres veces sin que nadie te haya explicado qué hace exactamente. Y la promesa de convertir src/catalogo-datos.js en un módulo reutilizable y de llevar las clases Evento y Sesion a src/dominio/ sigue pendiente. En la siguiente lección, Módulos CommonJS y require(), cerraremos esa deuda: entenderás el envoltorio que Node añade a cada fichero, por qué exports = ... no funciona y module.exports = ... sí, cómo require encuentra lo que busca, y por qué un módulo es en realidad un singleton.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados