Les promeses van resoldre l'asincronia d'un sol resultat: demanes una cosa, esperes, arriba el valor o l'error, s'ha acabat. Però bona part del que passa en un sistema real no té aquesta forma. Una sessió que s'exhaureix, una venda que es registra, un aforament que baixa del 10 %, un socket que rep dades, un fitxer que es modifica: això no és un resultat futur, és un flux de successos que passen moltes vegades i en moments impredictibles.

Node.js té un mecanisme propi per a això, i no és cap accessori: és una de les seves peces fundacionals. El mòdul events i la seva classe EventEmitter són per sota dels streams, del servidor HTTP, dels sockets, dels processos fills i del mateix objecte process. Quan escrius servidor.on('request', ...) estàs fent servir exactament la mateixa API que aprendràs aquí.

En aquesta lliçó construiràs el GestorDeVendes d'Escena Viva: una classe que hereta d'EventEmitter i que avisa la resta del sistema quan es registra una venda, quan l'aforament d'una sessió baixa del 10 % i —el compromís que vam adquirir al Mòdul 1— quan una sessió s'exhaureix. Pel camí descobriràs que els oients s'executen de manera síncrona, per què l'esdeveniment error és especial fins al punt de tombar el procés, com es produeixen les fuites de memòria per oients oblidats, i com esperar un esdeveniment amb await.

Contingut

  1. El patró observador i per què Node el porta a l'ADN
  2. L'API d'EventEmitter
  3. Els oients s'executen de manera síncrona
  4. Arguments a emit i el valor de this
  5. Heretar d'EventEmitter: el GestorDeVendes d'Escena Viva
  6. L'esdeveniment error: l'únic que et pot matar
  7. Fuites de memòria per oients
  8. events.once(): esperar un esdeveniment amb await
  9. Esdeveniments, callbacks o promeses: taula de decisió
  10. Tot Node és un EventEmitter

  1. El patró observador i per què Node el porta a l'ADN

El patró observador resol un problema de disseny concret: un objecte necessita avisar-ne d'altres que ha passat alguna cosa, sense saber qui són ni quants n'hi ha.

Sense el patró, el codi queda així:

// MALAMENT: el gestor de vendes coneix tots els seus consumidors.
function registrarVenda(sessioId, quantitat) {
  const sessio = cercarSessio(sessioId);
  sessio.venudes += quantitat;

  if (sessio.venudes >= sessio.aforament) {
    correu.avisarOrganitzador(sessio);          // Acoblament 1
    cataleg.marcarComExhaurida(sessio.id);      // Acoblament 2
    metriques.comptarExhauriment(sessio.id);    // Acoblament 3
    auditoria.registrar('exhaurida', sessio);   // Acoblament 4
  }
}

Cada nova necessitat —«avisa també el tauler de l'administrador», «publica-ho a la memòria cau»— obliga a modificar la funció de vendes, que no té res a veure amb correus ni amb mètriques. És la recepta perquè un mòdul central acabi important mitja aplicació.

Amb el patró observador:

// BE: el gestor nomes anuncia. No sap qui escolta.
function registrarVenda(sessioId, quantitat) {
  const sessio = cercarSessio(sessioId);
  sessio.venudes += quantitat;

  if (sessio.venudes >= sessio.aforament) {
    this.emit('sessio-exhaurida', { sessioId, aforament: sessio.aforament });
  }
}

I cada interessat s'hi subscriu pel seu compte:

gestor.on('sessio-exhaurida', (dades) => correu.avisarOrganitzador(dades));
gestor.on('sessio-exhaurida', (dades) => metriques.comptarExhauriment(dades.sessioId));
flowchart LR
    G["GestorDeVendes<br/><i>emissor</i>"] -->|"emit('sessio-exhaurida')"| E{{"Esdeveniment"}}
    E --> O1["Avis per correu<br/>a l'organitzador"]
    E --> O2["Actualitzar el<br/>cataleg public"]
    E --> O3["Registrar metrica"]
    E --> O4["Auditoria"]

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

Els dos rols del patró:

Rol Qui és Què fa
Emissor (subject) GestorDeVendes Anuncia que ha passat alguna cosa. No coneix els oients
Oient (observer) Correu, mètriques, auditoria… Se subscriu als esdeveniments que li interessen

El guany és el desacoblament: pots afegir, treure o provar oients sense tocar ni una línia de l'emissor. Al Mòdul 9 això serà decisiu, perquè provar el GestorDeVendes no requerirà cap servei de correu.

Node porta aquest patró a l'ADN per una raó històrica: és la manera natural d'expressar E/S asíncrona repetida. Un socket no lliura «un resultat»: lliura dades moltes vegades, després es tanca, i pel camí pot fallar. Això són tres esdeveniments diferents, no pas una promesa.

  1. L'API d'EventEmitter

EventEmitter viu al mòdul events del nucli de Node.

// src/laboratori/emissor-basic.js
const EventEmitter = require('node:events');

const emissor = new EventEmitter();

// Registrar un oient
emissor.on('venda-registrada', (dades) => {
  console.log(`Venda de ${dades.quantitat} entrades a ${dades.sessioId}`);
});

// Emetre l'esdeveniment
emissor.emit('venda-registrada', { sessioId: 'ses-002-2', quantitat: 3 });
// Venda de 3 entrades a ses-002-2

Els mètodes que faràs servir:

Mètode Què fa Torna
on(esdeveniment, oient) Registra un oient. Àlies: addListener L'emissor (encadenable)
once(esdeveniment, oient) Registra un oient que s'executa un sol cop i s'elimina L'emissor
emit(esdeveniment, ...args) Dispara l'esdeveniment, cridant tots els oients en ordre true si hi havia oients, false si no
off(esdeveniment, oient) Elimina un oient concret. Àlies: removeListener L'emissor
removeAllListeners([esdeveniment]) Elimina tots els oients d'un esdeveniment (o de tots) L'emissor
listenerCount(esdeveniment) Quants oients té aquest esdeveniment Número
eventNames() Noms de tots els esdeveniments amb oients Array
prependListener(esdeveniment, oient) Registra un oient al principi de la llista L'emissor
setMaxListeners(n) Canvia el llindar de l'avís de fuita (per defecte 10) L'emissor

on contra once

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

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

for (let i = 1; i <= 3; i++) {
  console.log(`Emetent venda ${i} (oients: ${emissor.listenerCount('venda')})`);
  emissor.emit('venda', i);
}
Emetent venda 1 (oients: 2)
  [on]   venda numero 1
  [once] venda numero 1
Emetent venda 2 (oients: 1)
  [on]   venda numero 2
Emetent venda 3 (oients: 1)
  [on]   venda numero 3

once es desregistra sol després de la primera execució. És l'elecció correcta per a successos que passen una única vegada: 'llest', 'connectat', 'tancat'.

Desregistrar oients

Per poder treure un oient necessites la mateixa referència de funció amb què el vas registrar:

// MALAMENT: son dues funcions diferents. L'off no fa res.
emissor.on('venda', (n) => console.log(n));
emissor.off('venda', (n) => console.log(n));
console.log(emissor.listenerCount('venda'));   // 1 <- encara hi es

// BE: guardem la referencia.
function enVendre(n) {
  console.log(n);
}
emissor.on('venda', enVendre);
emissor.off('venda', enVendre);
console.log(emissor.listenerCount('venda'));   // 0

És una de les causes més comunes de fuites de memòria per oients, i hi tornarem a l'apartat 7.

emit et diu si algú escoltava

const hiHaviaOients = emissor.emit('esdeveniment-sense-oients');
console.log(hiHaviaOients);   // false

Emetre un esdeveniment sense oients no és cap error: simplement no passa res. Amb una excepció molt important, l'esdeveniment error, que veurem a l'apartat 6.

  1. Els oients s'executen de manera síncrona

Aquesta és la característica d'EventEmitter que més sorprèn, i la que té conseqüències de rendiment reals:

emit crida tots els oients de manera síncrona, l'un darrere l'altre, en l'ordre en què es van registrar, i no torna fins que tots han acabat.

EventEmitter no és asíncron. És un mecanisme de despatx síncron que resulta que es fa servir molt en contextos asíncrons.

// src/laboratori/oients-sincrons.js
const EventEmitter = require('node:events');
const emissor = new EventEmitter();

emissor.on('venda', () => console.log('  oient 1 (registrat primer)'));
emissor.on('venda', () => console.log('  oient 2 (registrat segon)'));
emissor.on('venda', () => console.log('  oient 3 (registrat tercer)'));

console.log("Abans d'emit");
emissor.emit('venda');
console.log("Despres d'emit");
Abans d'emit
  oient 1 (registrat primer)
  oient 2 (registrat segon)
  oient 3 (registrat tercer)
Despres d'emit

Els tres oients es van executar abans que emit tornés el control. Res de bucle d'esdeveniments, res de microtasques.

La implicació per al rendiment

Si emit és síncron, un oient lent bloqueja tots els altres i el fil principal. Tot el que vas aprendre a la lliçó d'arquitectura s'hi aplica:

// src/laboratori/oient-lent.js
const EventEmitter = require('node:events');
const emissor = new EventEmitter();

emissor.on('venda', () => console.log('  rapid 1'));

emissor.on('venda', () => {
  // Un oient que fa feina pesada de manera sincrona.
  const limit = Date.now() + 200;
  while (Date.now() < limit) { /* generar el PDF de l'entrada, per exemple */ }
  console.log('  LENT (200 ms)');
});

emissor.on('venda', () => console.log('  rapid 2'));

const inici = Date.now();
emissor.emit('venda');
console.log(`emit ha trigat ${Date.now() - inici} ms`);
  rapid 1
  LENT (200 ms)
  rapid 2
emit ha trigat 201 ms

L'emit va trigar 201 ms. Si això passés dins d'una petició HTTP d'Escena Viva, el servidor sencer s'aturaria 200 ms per cada venda.

La regla d'or per escriure oients:

Un oient ha de ser ràpid. Si té feina pesada, que la delegui.

// MALAMENT: feina pesada sincrona dins de l'oient.
gestor.on('sessio-exhaurida', (dades) => {
  generarInformePdfSincron(dades);   // Bloqueja tothom
});

// BE: l'oient nomes encua la feina i torna el control.
gestor.on('sessio-exhaurida', (dades) => {
  setImmediate(() => generarInformePdf(dades));
});

// MILLOR: feina asincrona de debo, amb la seva propia gestio d'errors.
gestor.on('sessio-exhaurida', (dades) => {
  enviarCorreuOrganitzador(dades).catch((error) => {
    console.error(`[correu] fallada en avisar de ${dades.sessioId}: ${error.message}`);
  });
});

Fixa't en aquest .catch de l'últim exemple. És obligatori. Un oient async la promesa del qual es rebutgi produeix un rebuig no gestionat que, com vas aprendre a la lliçó anterior, tomba el procés. I emit no et pot ajudar: va tornar el control molt abans que la promesa s'assentés.

L'ordre importa

Com que els oients s'executen en ordre de registre, aquest ordre és part del comportament observable:

emissor.on('venda', () => console.log('normal'));
emissor.prependListener('venda', () => console.log('primer, sempre'));
emissor.emit('venda');
// primer, sempre
// normal

Fer servir prependListener per «colar-se» al davant sol ser senyal d'un disseny fràgil. Si l'ordre dels oients importa de debò, probablement el que necessites no és un esdeveniment sinó una seqüència explícita de passos.

  1. Arguments a emit i el valor de this

4.1 Passar dades

emit accepta qualsevol nombre d'arguments després del nom de l'esdeveniment, i tots arriben a cada oient:

emissor.emit('venda-registrada', 'ses-002-2', 3, 5400);

emissor.on('venda-registrada', (sessioId, quantitat, importCentims) => {
  console.log(`${quantitat} entrades de ${sessioId} per ${importCentims} centims`);
});

Funciona, però a Escena Viva farem servir sempre un únic objecte:

emissor.emit('venda-registrada', {
  sessioId: 'ses-002-2',
  esdevenimentId: 'evt-002',
  quantitat: 3,
  importCentims: 5400,
  lliuresRestants: 72,
  dataHora: '2026-08-11T18:42:11'
});

Les raons són les mateixes que per a qualsevol API:

Arguments solts Un objecte
L'ordre importa i és fàcil equivocar-se Els noms s'autodocumenten
Afegir un camp trenca tots els oients Afegir un camp no trenca res
Un oient que només vol la tercera dada ha de declarar les tres Desestructura només el que necessita
// L'oient pren nomes el que li interessa.
gestor.on('venda-registrada', ({ sessioId, lliuresRestants }) => {
  console.log(`${sessioId}: en queden ${lliuresRestants}`);
});

4.2 El valor de this

Dins d'un oient registrat amb function, this és l'emissor:

// src/laboratori/this-en-oients.js
const EventEmitter = require('node:events');
const emissor = new EventEmitter();

emissor.on('venda', function () {
  console.log('Amb function, this es:', this.constructor.name);
  console.log('  esdeveniments registrats:', this.eventNames());
});

emissor.on('venda', () => {
  // Una funcio fletxa NO te this propi: hereta el de l'ambit on es va escriure.
  console.log('Amb fletxa, this es:', this);
});

emissor.emit('venda');
Amb function, this es: EventEmitter
  esdeveniments registrats: [ 'venda' ]
Amb fletxa, this es: {}

Node vincula this a l'emissor quan l'oient és una funció normal. Amb una funció fletxa, this és el de l'àmbit circumdant —en un mòdul CommonJS de nivell superior, module.exports, que apareix com a {}—.

function () {} () => {}
this L'emissor El de l'àmbit on es va escriure
Accés a this.eventNames(), this.off(...) Sí No
Dins d'un mètode de classe this deixa de ser la instància this continua sent la instància

Aquest últim punt és el que decideix a la pràctica. Dins d'una classe, la fletxa és gairebé sempre el que vols:

class PanellDeControl {
  constructor(gestor) {
    this.exhaurides = [];

    // Amb fletxa: this continua sent el PanellDeControl. Correcte.
    gestor.on('sessio-exhaurida', (dades) => {
      this.exhaurides.push(dades.sessioId);
    });

    // Amb function: this seria el gestor, i this.exhaurides seria undefined.
    // gestor.on('sessio-exhaurida', function (dades) {
    //   this.exhaurides.push(dades.sessioId);   // TypeError
    // });
  }
}

Recomanació per al curs: fes servir funcions fletxa als oients. L'accés a this com a emissor gairebé mai no cal, i quan el necessites tens la referència a l'emissor en una variable de totes maneres.

  1. Heretar d'EventEmitter: el GestorDeVendes d'Escena Viva

Arribem a l'exemple central de la lliçó i al compromís que vam adquirir al Mòdul 1. Crearem una classe de domini que és un emissor d'esdeveniments.

Crea src/domini/gestor-vendes.js:

// src/domini/gestor-vendes.js
// Registra les vendes d'Escena Viva i avisa la resta del sistema
// mitjancant esdeveniments, sense coneixer cap dels seus consumidors.

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

// Llindar per sota del qual es considera que l'aforament esta "baix".
const LLINDAR_AFORAMENT_BAIX = 0.10;   // 10 %

class GestorDeVendes extends EventEmitter {
  // Sessions indexades per id, per a acces directe.
  #sessions = new Map();

  // Comptador de codis d'entrada emesos aquest any.
  #sequenciaEntrada = 0;

  constructor(esdeveniments = []) {
    // super() es OBLIGATORI i ha d'anar abans d'usar this:
    // inicialitza la maquinaria interna d'EventEmitter.
    super();

    for (const esdeveniment of esdeveniments) {
      for (const sessio of esdeveniment.sessions) {
        this.#sessions.set(sessio.id, { ...sessio, esdevenimentId: esdeveniment.id, titol: esdeveniment.titol });
      }
    }
  }

  get nombreSessions() {
    return this.#sessions.size;
  }

  obtenirSessio(sessioId) {
    return this.#sessions.get(sessioId);
  }

  // Genera un codi amb el format del projecte: EV-2026-000123
  #generarCodiEntrada() {
    this.#sequenciaEntrada++;
    const any = new Date().getFullYear();
    return `EV-${any}-${String(this.#sequenciaEntrada).padStart(6, '0')}`;
  }

  /**
   * Registra la venda de "quantitat" entrades d'una sessio.
   * Emet: 'venda-registrada' sempre; a mes 'aforament-baix' o 'sessio-exhaurida'
   * si la venda creua aquests llindars.
   * Torna les entrades emeses.
   */
  registrarVenda(sessioId, quantitat, comandaId) {
    const sessio = this.#sessions.get(sessioId);

    // Els errors d'US es llancen: son fallades del programador, no successos.
    if (!sessio) {
      const error = new Error(`Sessio no trobada: ${sessioId}`);
      error.codi = 'SESSIO_NO_TROBADA';
      throw error;
    }
    if (!Number.isInteger(quantitat) || quantitat < 1) {
      const error = new Error('La quantitat ha de ser un enter positiu');
      error.codi = 'QUANTITAT_INVALIDA';
      throw error;
    }

    const lliuresAbans = sessio.aforament - sessio.venudes;

    if (quantitat > lliuresAbans) {
      const error = new Error(
        `Aforament insuficient a ${sessioId}: en demanes ${quantitat} i en queden ${lliuresAbans}`
      );
      error.codi = 'AFORAMENT_INSUFICIENT';
      throw error;
    }

    // El JavaScript d'usuari es monofil: aquesta actualitzacio es atomica.
    sessio.venudes += quantitat;
    const lliuresDespres = sessio.aforament - sessio.venudes;

    const entrades = [];
    for (let i = 0; i < quantitat; i++) {
      entrades.push({
        codi: this.#generarCodiEntrada(),
        sessioId,
        comandaId,
        estat: 'valida'
      });
    }

    // --- Anunci del que ha passat ---

    this.emit('venda-registrada', {
      sessioId,
      esdevenimentId: sessio.esdevenimentId,
      titol: sessio.titol,
      quantitat,
      importCentims: quantitat * sessio.preuCentims,
      lliuresRestants: lliuresDespres,
      ocupacio: Math.round((sessio.venudes / sessio.aforament) * 100)
    });

    const proporcioLliure = lliuresDespres / sessio.aforament;
    const proporcioLliureAbans = lliuresAbans / sessio.aforament;

    if (lliuresDespres === 0) {
      // Exhaurida: l'esdeveniment que vam prometre al modul 1.
      this.emit('sessio-exhaurida', {
        sessioId,
        esdevenimentId: sessio.esdevenimentId,
        titol: sessio.titol,
        aforament: sessio.aforament,
        recaptacioCentims: sessio.venudes * sessio.preuCentims
      });
    } else if (proporcioLliure < LLINDAR_AFORAMENT_BAIX && proporcioLliureAbans >= LLINDAR_AFORAMENT_BAIX) {
      // Nomes en CREUAR el llindar, no a cada venda posterior.
      this.emit('aforament-baix', {
        sessioId,
        esdevenimentId: sessio.esdevenimentId,
        titol: sessio.titol,
        lliuresRestants: lliuresDespres,
        percentatgeLliure: Math.round(proporcioLliure * 100)
      });
    }

    return entrades;
  }
}

module.exports = { GestorDeVendes, LLINDAR_AFORAMENT_BAIX };

Aquesta última línia, module.exports, és el que converteix el fitxer en un mòdul reutilitzable. L'estudiarem a fons a la lliçó Mòduls CommonJS i require(); de moment accepta que exposa la classe perquè altres fitxers la puguin fer servir amb require.

Ara els consumidors. Crea src/laboratori/venda-amb-esdeveniments.js:

// src/laboratori/venda-amb-esdeveniments.js
const { GestorDeVendes } = require('../domini/gestor-vendes.js');

const cataleg = [
  {
    id: 'evt-002',
    titol: 'Noche de Monologos',
    sessions: [
      { id: 'ses-002-1', dataHora: '2026-10-10T21:30:00', aforament: 120, venudes: 100, preuCentims: 1800 },
      { id: 'ses-002-2', dataHora: '2026-10-11T21:30:00', aforament: 120, venudes: 45,  preuCentims: 1800 }
    ]
  }
];

const gestor = new GestorDeVendes(cataleg);

// --- Oients: cadascun amb una unica responsabilitat ---

// 1. Registre d'operacio, per stderr.
gestor.on('venda-registrada', ({ sessioId, quantitat, importCentims, ocupacio }) => {
  console.error(
    `[venda] ${quantitat} entrades de ${sessioId} per ` +
    `${(importCentims / 100).toFixed(2)} EUR (${ocupacio}% ocupat)`
  );
});

// 2. Avis comercial a l'organitzador.
gestor.on('aforament-baix', ({ titol, sessioId, lliuresRestants, percentatgeLliure }) => {
  console.error(
    `[avis] "${titol}" (${sessioId}) al ${100 - percentatgeLliure}%: ` +
    `nomes queden ${lliuresRestants} entrades`
  );
});

// 3. Sessio exhaurida: diversos oients independents per al MATEIX esdeveniment.
gestor.on('sessio-exhaurida', ({ titol, sessioId, recaptacioCentims }) => {
  console.log(
    `EXHAURIDA: "${titol}" (${sessioId}). ` +
    `Recaptacio: ${(recaptacioCentims / 100).toFixed(2)} EUR`
  );
});

gestor.on('sessio-exhaurida', ({ sessioId }) => {
  console.error(`[cataleg] marcant ${sessioId} com a no disponible`);
});

gestor.on('sessio-exhaurida', ({ sessioId }) => {
  // Feina pesada: SEMPRE delegada, mai sincrona dins de l'oient.
  setImmediate(() => {
    console.error(`[correu] avis d'exhauriment enviat per a ${sessioId}`);
  });
});

// --- Simulacio de l'obertura de venda ---

console.error(`Gestor amb ${gestor.nombreSessions} sessions. Comenca la venda.`);
console.error('');

gestor.registrarVenda('ses-002-1', 5, 'com-001');    // 105/120: 12,5% lliure
gestor.registrarVenda('ses-002-1', 4, 'com-002');    // 109/120: 9,2% lliure -> aforament-baix
gestor.registrarVenda('ses-002-1', 6, 'com-003');    // 115/120: continua baix, NO reemet
gestor.registrarVenda('ses-002-1', 5, 'com-004');    // 120/120 -> sessio-exhaurida

// Un error d'us: es llanca, no s'emet.
try {
  gestor.registrarVenda('ses-002-1', 1, 'com-005');
} catch (error) {
  console.error(`[error] [${error.codi}] ${error.message}`);
}
Gestor amb 2 sessions. Comenca la venda.

[venda] 5 entrades de ses-002-1 per 90.00 EUR (88% ocupat)
[venda] 4 entrades de ses-002-1 per 72.00 EUR (91% ocupat)
[avis] "Noche de Monologos" (ses-002-1) al 91%: nomes queden 11 entrades
[venda] 6 entrades de ses-002-1 per 108.00 EUR (96% ocupat)
[venda] 5 entrades de ses-002-1 per 90.00 EUR (100% ocupat)
EXHAURIDA: "Noche de Monologos" (ses-002-1). Recaptacio: 2160.00 EUR
[cataleg] marcant ses-002-1 com a no disponible
[error] [AFORAMENT_INSUFICIENT] Aforament insuficient a ses-002-1: en demanes 1 i en queden 0
[correu] avis d'exhauriment enviat per a ses-002-1

Cinc decisions de disseny que val la pena assenyalar:

  1. super() és obligatori i ha d'anar abans de qualsevol ús de this. Sense ell, la maquinària interna d'EventEmitter no s'inicialitza i emit falla.
  2. Noms d'esdeveniment en kebab-case: venda-registrada, aforament-baix, sessio-exhaurida. És la convenció de l'ecosistema i evita problemes en fer-los servir com a cadenes.
  3. aforament-baix només s'emet en creuar el llindar. Si s'emetés a cada venda per sota del 10 %, l'organitzador rebria vint correus. Detectar la transició en comptes de l'estat és un detall que separa un avís útil d'una molèstia.
  4. Els errors d'ús es llancen, no s'emeten. Un aforament insuficient és un error del codi que crida; una sessió exhaurida és un succés del domini. Confondre'ls és la fallada de disseny més comuna amb EventEmitter.
  5. Tres oients diferents per a sessio-exhaurida, cadascun amb una responsabilitat. El gestor no en coneix cap, i afegir-ne un quart no requereix tocar-lo.

Fixa't també en l'ordre de la sortida: el [correu] apareix al final, després fins i tot de l'error. És el setImmediate fent la seva feina: cedeix el control al bucle d'esdeveniments i s'executa a la fase check, quan tot el que era síncron ja ha acabat. Exactament el que vas aprendre a la lliçó del bucle d'esdeveniments.

  1. L'esdeveniment error: l'únic que et pot matar

EventEmitter tracta un nom d'esdeveniment de manera especial: error.

Si s'emet 'error' i no hi ha cap oient registrat, EventEmitter llança l'error. Si ningú no el captura, el procés mor.

// src/laboratori/esdeveniment-error.js
const EventEmitter = require('node:events');
const emissor = new EventEmitter();

emissor.emit('esdeveniment-qualsevol');   // Sense oients: no passa res, torna false

emissor.emit('error', new Error('La passarella de pagament no respon'));
// El proces MOR aqui.

console.log("Aixo no s'imprimeix mai");
node:events:496
      throw er; // Unhandled 'error' event
      ^
Error: La passarella de pagament no respon
    at Object.<anonymous> ...
Emitted 'error' event on EventEmitter instance at:
    ...
[el proces mor amb codi 1]

Per què un disseny tan dràstic? Per la mateixa filosofia que fa que els rebuigs no gestionats tombin el procés: un error que ningú no mira és pitjor que un procés caigut. Un servidor que continua dret ignorant errors de connexió acumula estat corrupte i falla de maneres incomprensibles hores després. Node prefereix fallar sorollosament.

La solució és trivial, i és obligatòria en qualsevol emissor que pugui fallar:

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

emissor.emit('error', new Error('La passarella de pagament no respon'));
// Ara es tracta correctament i el proces continua.

Aplicat al GestorDeVendes, per a fallades asíncrones que no es poden llançar:

// Dins de la classe: una fallada en un proces de fons s'anuncia com a esdeveniment.
sincronitzarAmbPassarella()
  .catch((error) => {
    error.codi = error.codi || 'SINCRONITZACIO_FALLIDA';
    this.emit('error', error);
  });

I al consumidor, sense excepció:

const gestor = new GestorDeVendes(cataleg);

// SEMPRE, abans de res.
gestor.on('error', (error) => {
  console.error(`[gestor] [${error.codi || 'ERROR'}] ${error.message}`);
  process.exitCode = 1;
});

Quan llançar i quan emetre error:

Situació Què fer
Argument invàlid, precondició incomplerta (fallada del programador) throw
Fallada asíncrona en un procés en marxa (xarxa caiguda, disc ple) emit('error', ...)
Succés de negoci esperat (aforament exhaurit) emit('sessio-exhaurida', ...), no és cap error

A la lliçó de Gestió d'Errors del Mòdul 6 formalitzarem aquesta distinció entre errors de programació i errors operatius.

  1. Fuites de memòria per oients

Un oient registrat és una referència viva: manté en memòria la funció i tot el que el seu tancament captura. Si registres oients i no els treus mai, la memòria creix sense parar. És la fuita de memòria més comuna en aplicacions Node.

Node t'avisa quan fa mala olor:

// src/laboratori/fuita-oients.js
const EventEmitter = require('node:events');
const gestor = new EventEmitter();

// Simulem una peticio HTTP que registra un oient i s'oblida de treure'l.
function atendrePeticio(numero) {
  const dadesDeLaPeticio = new Array(1000).fill(`peticio-${numero}`);

  gestor.on('sessio-exhaurida', () => {
    // El tancament reté dadesDeLaPeticio: 1000 elements per peticio.
    console.log(`Peticio ${numero} assabentada (${dadesDeLaPeticio.length} dades retingudes)`);
  });
}

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

L'avís salta a l'onzè oient del mateix esdeveniment. És només un avís: el programa continua funcionant i els oients es continuen afegint. Però és un senyal gairebé sempre correcte que alguna cosa s'està registrant i no es treu mai.

Les tres respostes possibles

1. L'avís té raó: hi ha una fuita. Desregistra.

function atendrePeticio(numero) {
  const enExhaurirse = (dades) => {
    console.log(`Peticio ${numero}: ${dades.sessioId} exhaurida`);
  };

  gestor.on('sessio-exhaurida', enExhaurirse);

  // En acabar la peticio, treiem l'oient.
  return function acabar() {
    gestor.off('sessio-exhaurida', enExhaurirse);
  };
}

const acabar = atendrePeticio(1);
// ... s'aten la peticio ...
acabar();

2. El succés passa un sol cop: fes servir once.

// once es desregistra sol. Sense cap fuita possible.
gestor.once('sessio-exhaurida', (dades) => {
  console.log(`Primera sessio exhaurida del dia: ${dades.sessioId}`);
});

3. Realment necessites més de deu oients: puja el límit conscientment.

// Nomes si saps que 50 oients es el disseny correcte, no un peda.
gestor.setMaxListeners(50);

// O globalment, per a tots els emissors (poques vegades justificat):
// require('node:events').defaultMaxListeners = 20;

No pugis mai el límit per «treure l'avís». L'avís existeix per avisar-te. Si el silencies sense entendre per què salta, has canviat un missatge molest per una fuita silenciosa que tombarà el procés en producció a les tres de la matinada.

Diagnòstic

// Eines d'inspeccio, utils quan salta l'avis.
console.log(gestor.eventNames());
// [ 'sessio-exhaurida', 'venda-registrada', 'error' ]

console.log(gestor.listenerCount('sessio-exhaurida'));
// 12

console.log(gestor.listeners('sessio-exhaurida').map((f) => f.name));
// [ 'enExhaurirse', 'enExhaurirse', ... ]  <- 12 vegades la mateixa: sospitos

Si en un servidor veus que listenerCount creix amb el nombre de peticions ateses, tens una fuita confirmada. És exactament el tipus de problema que es detecta amb la tendència de heapUsed de la primera lliçó del mòdul.

  1. events.once(): esperar un esdeveniment amb await

De vegades només vols esperar que passi una cosa un cop. Amb l'API d'oients això obliga a embolcallar-ho tot en un callback. El mòdul events ofereix un pont cap a les promeses:

// src/laboratori/esperar-esdeveniment.js
const { once } = require('node:events');
const { GestorDeVendes } = require('../domini/gestor-vendes.js');

const gestor = new GestorDeVendes(cataleg);

async function esperarPrimerExhauriment() {
  console.log("Esperant que s'exhaureixi la primera sessio...");

  // once() torna una PROMESA que es compleix amb un ARRAY:
  // els arguments amb que es va emetre l'esdeveniment.
  const [dades] = await once(gestor, 'sessio-exhaurida');

  console.log(`Exhaurida: ${dades.titol} (${dades.sessioId})`);
  return dades;
}

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

// Provoquem l'exhauriment una mica despres.
setTimeout(() => {
  gestor.registrarVenda('ses-002-1', 20, 'com-100');
}, 300);
Esperant que s'exhaureixi la primera sessio...
Exhaurida: Noche de Monologos (ses-002-1)

Detalls importants d'events.once():

  • Torna un array, perquè emit pot passar diversos arguments. Per això es desestructura amb const [dades] = ....
  • Rebutja automàticament si l'emissor emet 'error' mentre espera. Aquest comportament és molt convenient: no has de gestionar dos casos a mà.
  • Accepta un senyal de cancel·lació per no esperar indefinidament:
const controlador = new AbortController();
setTimeout(() => controlador.abort(), 5000);

try {
  const [dades] = await once(gestor, 'sessio-exhaurida', { signal: controlador.signal });
  console.log(`Exhaurida: ${dades.sessioId}`);
} catch (error) {
  if (error.name === 'AbortError') {
    console.error("Cap sessio no s'ha exhaurit en 5 segons");
  } else {
    throw error;
  }
}

I el seu company events.on(), que torna un iterador asíncron per consumir un flux d'esdeveniments amb for await:

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

async function seguirVendes(gestor) {
  // Consumeix TOTES les vendes segons van passant.
  for await (const [dades] of on(gestor, 'venda-registrada')) {
    console.log(`${dades.quantitat} entrades de ${dades.sessioId}`);
  }
}

Compte amb for await: aquest bucle no acaba mai per si sol. Necessita un senyal de cancel·lació o un break. I mentre processes un esdeveniment, els que arribin s'acumulen en un búfer intern; si el teu processament és més lent que l'emissió, aquest búfer creix sense límit.

  1. Esdeveniments, callbacks o promeses: taula de decisió

Ja tens les tres eines. Triar bé és una decisió de disseny, no pas de gust.

Criteri Callback Promesa / async-await EventEmitter
Nombre de resultats Un (per crida) Exactament un Molts, al llarg del temps
Nombre d'interessats Un Molts (diversos .then) Molts, i desconeguts
Qui decideix què passa després Qui crida Qui crida Cada oient, pel seu compte
Acoblament Directe Directe Nul
Execució Asíncrona Asíncrona (microtasques) Síncrona
Gestió d'errors (error, ...) try/catch Esdeveniment error (o mor!)
Es pot esperar amb await? Amb promisify Nativament Amb events.once()
Es pot arribar tard No Sí (la promesa guarda el resultat) No (et perds el que s'ha emès)

I la guia pràctica:

Si el teu cas és… Fes servir
«Dona'm l'esdeveniment evt-002» Promesa
«Cobra aquesta comanda i digue'm si ha funcionat» Promesa
«Avisa'm cada cop que arribi un tros de dades» EventEmitter (streams)
«Avisa'm quan una sessió s'exhaureixi, sigui quan sigui» EventEmitter
«Vull que diverses parts del sistema reaccionin sense conèixer-se» EventEmitter
«L'API de Node només accepta callback» Callback (o promisify)
«Necessito reaccionar al que passi, una única vegada» events.once() + await

La regla d'una línia:

Una resposta → promesa. Moltes notificacions → esdeveniment.

I un antipatró per evitar: no facis servir esdeveniments per al flux principal d'una operació. Emetre 'comanda-creada' i esperar que un oient continuï el flux de compra converteix un procés lineal i depurable en una cadena invisible de salts. Els esdeveniments són per a efectes secundaris desacoblats —notificar, registrar, mesurar—, no pas per partir en trossos una cosa que hauria de llegir-se de dalt a baix.

  1. Tot Node és un EventEmitter

Quan entens EventEmitter desbloqueges mitja biblioteca estàndard, perquè gairebé tot n'hereta:

// L'objecte process: un EventEmitter.
process.on('exit', (codi) => console.error(`Sortint amb codi ${codi}`));
process.on('SIGINT', () => {
  console.error('Ctrl+C rebut: tancant ordenadament...');
  process.exit(0);
});
process.on('unhandledRejection', (motiu) => console.error(motiu));
// Un servidor HTTP: un EventEmitter. (Modul 4)
const http = require('node:http');
const servidor = http.createServer();

servidor.on('request', (peticio, resposta) => {
  resposta.end('Escena Viva');
});
servidor.on('listening', () => console.error('Servidor a punt'));
servidor.on('close', () => console.error('Servidor tancat'));
servidor.on('error', (error) => console.error(error.message));
// Un stream de lectura: un EventEmitter. (Modul 3)
const fs = require('node:fs');
const lector = fs.createReadStream('dades/vendes.csv');

lector.on('data', (tros) => console.error(`Rebuts ${tros.length} bytes`));
lector.on('end', () => console.error('Fitxer complet'));
lector.on('error', (error) => console.error(`Error de lectura: ${error.message}`));
Objecte de Node Esdeveniments habituals Mòdul del curs
process exit, SIGINT, uncaughtException, unhandledRejection 2 i 11
Servidor HTTP request, listening, close, error 4
Streams data, end, error, close, finish 3
Sockets TCP connect, data, end, error 4
Processos fills exit, close, message, error 10
fs.watch change, rename 3

Això vol dir que el que s'ha après aquí no és específic de GestorDeVendes: és el vocabulari amb què Node parla amb tu a tots els mòduls que queden. Quan al Mòdul 3 llegeixis lector.on('data', ...), ja sabràs que és un oient síncron en una llista, que l'ordre de registre importa, que un oient lent bloqueja el procés i que l'esdeveniment error s'ha de registrar sempre.

Errors Comuns i Consells

Error 1: no registrar un oient per a error. L'únic esdeveniment que mata el procés si ningú no l'escolta. En tot emissor que pugui fallar: emissor.on('error', ...).

Error 2: creure que emit és asíncron. És completament síncron. Un oient lent bloqueja el fil principal i tots els altres oients.

Error 3: fer feina pesada dins d'un oient. Delega amb setImmediate o amb una operació asíncrona real, i no oblidis el .catch.

Error 4: un oient async sense .catch. emit no espera la promesa i no en captura el rebuig. Un rebuig no gestionat tomba el procés.

Error 5: intentar treure un oient amb una funció diferent. off compara per referència. Guarda la funció en una variable si l'has de desregistrar.

Error 6: oblidar super() en heretar d'EventEmitter. ReferenceError: Must call super constructor o, pitjor, un emissor a mig inicialitzar.

Error 7: pujar setMaxListeners per silenciar l'avís. Canvia un missatge incòmode per una fuita silenciosa. Investiga primer per què s'acumulen.

Error 8: emetre un esdeveniment al constructor. Ningú no s'hi ha pogut subscriure encara. Si ho has de fer, ajorna-ho amb process.nextTick, com vas veure a la lliçó del bucle d'esdeveniments.

Error 9: fer servir esdeveniments per al flux principal. Converteix codi lineal en salts invisibles impossibles de seguir. Els esdeveniments són per a efectes secundaris desacoblats.

Consell 1: noms d'esdeveniment en kebab-case i en passat. venda-registrada, sessio-exhaurida: descriuen alguna cosa que ja ha passat, que és la semàntica correcta d'un esdeveniment.

Consell 2: emet sempre un únic objecte. Afegir camps no trenca ningú i els oients desestructuren el que necessiten.

Consell 3: emet a les transicions, no als estats. aforament-baix només en creuar el llindar, mai a cada venda posterior.

Consell 4: documenta els esdeveniments de les teves classes. Els esdeveniments són part de l'API pública, tant com els mètodes. Si no estan documentats, ningú no els farà servir.

Exercicis

Exercici 1: un tauler de control com a oient

Escriu src/laboratori/panell-vendes.js amb una classe PanellDeVendes que no hereti d'EventEmitter sinó que se subscrigui a un GestorDeVendes, i que mantingui:

  1. Total d'entrades venudes i recaptació acumulada en cèntims.
  2. Llista de sessions exhaurides i llista de sessions en avís d'aforament baix.
  3. Un mètode resum() que torni un objecte amb aquestes dades i la recaptació formatada en euros.
  4. Un mètode desconnectar() que desregistri tots els seus oients del gestor.

Requisits tècnics:

  • Els oients han de ser funcions fletxa o mètodes vinculats, de manera que this continuï sent el tauler.
  • desconnectar() ha de deixar gestor.listenerCount() amb el mateix valor que tenia abans de connectar el tauler. Demostra-ho imprimint els comptadors abans, durant i després.
  • Simula una tanda de vendes que exhaureixi una sessió i verifica'n el resum.

Exercici 2: l'esdeveniment error i l'avís de fuita

Escriu src/laboratori/emissor-robust.js que demostri, en un únic programa i sense que el procés mori:

  1. Que emetre 'error' sense oient llançaria una excepció. Captura-la amb try/catch al voltant de l'emit i explica en un comentari per què allà sí que funciona el try/catch (pista: emit és síncron).
  2. Que amb un oient d''error' registrat el procés continua amb normalitat.
  3. Que registrar 11 oients del mateix esdeveniment produeix MaxListenersExceededWarning. Captura'l amb process.on('warning', ...) i imprimeix el nom de l'avís en lloc de deixar que surti per defecte.
  4. Que després de setMaxListeners(20) ja no apareix l'avís.
  5. Un petit diagnòstic final amb eventNames() i listenerCount() de cada esdeveniment.

Exercici 3: esperar l'exhauriment amb await i temps límit

Escriu src/laboratori/esperar-exhauriment.js que:

  1. Creï un GestorDeVendes amb les tres sessions d'evt-002 (aforaments 120, vendes inicials 118, 45 i 12).
  2. Implementi esperarExhauriment(gestor, limitMs) fent servir events.once() amb un AbortController per no esperar més de limitMs.
  3. Simuli vendes aleatòries cada 100 ms sobre sessions a l'atzar, amb quantitats d'1 a 5, fins que alguna cosa s'exhaureixi o venci el límit.
  4. Informi del resultat: quina sessió s'ha exhaurit i en quants mil·lisegons, o que ha vençut el temps límit.
  5. Tracti correctament l'AbortError (missatge clar, process.exitCode = 1) i qualsevol altre error (rellançar-lo).
  6. Netegi els temporitzadors en acabar perquè el procés surti sol.

Respon a més: per què events.once() és millor aquí que registrar un once amb callback? I què passaria si la sessió s'exhaurís abans de cridar esperarExhauriment?

Solucions

Solució 1

// src/laboratori/panell-vendes.js
// Un consumidor d'esdeveniments que no hereta d'EventEmitter: nomes escolta.

const { GestorDeVendes } = require('../domini/gestor-vendes.js');

class PanellDeVendes {
  #gestor;
  #connectat = false;

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

    this.entradesVenudes = 0;
    this.recaptacioCentims = 0;
    this.exhaurides = [];
    this.enAvis = [];

    // Guardem les referencies: son necessaries per poder desregistrar.
    // Les fletxes conserven "this" apuntant al panell.
    this.enVendres = ({ quantitat, importCentims }) => {
      this.entradesVenudes += quantitat;
      this.recaptacioCentims += importCentims;
    };

    this.enExhaurirse = ({ sessioId, titol }) => {
      this.exhaurides.push({ sessioId, titol });
    };

    this.enBaixarAforament = ({ sessioId, lliuresRestants }) => {
      this.enAvis.push({ sessioId, lliuresRestants });
    };

    this.connectar();
  }

  connectar() {
    if (this.#connectat) return;
    this.#gestor.on('venda-registrada', this.enVendres);
    this.#gestor.on('sessio-exhaurida', this.enExhaurirse);
    this.#gestor.on('aforament-baix', this.enBaixarAforament);
    this.#connectat = true;
  }

  desconnectar() {
    if (!this.#connectat) return;
    this.#gestor.off('venda-registrada', this.enVendres);
    this.#gestor.off('sessio-exhaurida', this.enExhaurirse);
    this.#gestor.off('aforament-baix', this.enBaixarAforament);
    this.#connectat = false;
  }

  resum() {
    return {
      entradesVenudes: this.entradesVenudes,
      recaptacioEuros: (this.recaptacioCentims / 100).toFixed(2),
      sessionsExhaurides: this.exhaurides.length,
      sessionsEnAvis: this.enAvis.length,
      exhaurides: this.exhaurides.map((a) => a.sessioId)
    };
  }
}

// --- Demostracio ---

const cataleg = [
  {
    id: 'evt-002',
    titol: 'Noche de Monologos',
    sessions: [
      { id: 'ses-002-1', dataHora: '2026-10-10T21:30:00', aforament: 120, venudes: 100, preuCentims: 1800 },
      { id: 'ses-002-2', dataHora: '2026-10-11T21:30:00', aforament: 120, venudes: 45,  preuCentims: 1800 }
    ]
  }
];

const gestor = new GestorDeVendes(cataleg);

function comptadors(etiqueta) {
  console.error(
    `${etiqueta.padEnd(24)} venda-registrada=${gestor.listenerCount('venda-registrada')} ` +
    `sessio-exhaurida=${gestor.listenerCount('sessio-exhaurida')} ` +
    `aforament-baix=${gestor.listenerCount('aforament-baix')}`
  );
}

comptadors('Abans de connectar:');
const panell = new PanellDeVendes(gestor);
comptadors('Amb el panell connectat:');

gestor.registrarVenda('ses-002-1', 8, 'com-001');    // 108/120 -> aforament-baix
gestor.registrarVenda('ses-002-1', 12, 'com-002');   // 120/120 -> exhaurida
gestor.registrarVenda('ses-002-2', 10, 'com-003');

console.log('');
console.log('RESUM DEL PANELL');
console.table([panell.resum()]);

panell.desconnectar();
comptadors('Despres de desconnectar:');

// Una venda mes: el panell ja no se n'ha d'assabentar.
gestor.registrarVenda('ses-002-2', 5, 'com-004');
console.log('');
console.log(`Entrades despres de desconnectar (no ha de canviar): ${panell.resum().entradesVenudes}`);
Abans de connectar:      venda-registrada=0 sessio-exhaurida=0 aforament-baix=0
Amb el panell connectat: venda-registrada=1 sessio-exhaurida=1 aforament-baix=1

RESUM DEL PANELL
┌─────────┬─────────────────┬─────────────────┬────────────────────┬────────────────┬───────────────┐
│ (index) │ entradesVenudes │ recaptacioEuros │ sessionsExhaurides │ sessionsEnAvis │ exhaurides    │
├─────────┼─────────────────┼─────────────────┼────────────────────┼────────────────┼───────────────┤
│ 0       │ 30              │ '540.00'        │ 1                  │ 1              │ ['ses-002-1'] │
└─────────┴─────────────────┴─────────────────┴────────────────────┴────────────────┴───────────────┘

Despres de desconnectar: venda-registrada=0 sessio-exhaurida=0 aforament-baix=0

Entrades despres de desconnectar (no ha de canviar): 30

La clau és guardar les referències de les funcions en propietats del tauler. Si els oients s'haguessin registrat com a fletxes anònimes dins de connectar(), desconnectar() no tindria amb què cridar off i els comptadors no tornarien a zero. Aquesta disciplina és exactament la que evita les fuites de memòria de l'apartat 7.

Solució 2

// src/laboratori/emissor-robust.js
// Demostra el comportament de l'esdeveniment 'error' i de l'avis de fuita d'oients.

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

// Interceptem els avisos de Node per tractar-los nosaltres (punt 3).
process.on('warning', (avis) => {
  console.error(`[avis capturat] ${avis.name}: ${avis.message.split('.')[0]}.`);
});

// --- 1. Emetre 'error' sense oient ---
console.log('1. Emetre "error" sense oient registrat');

const emissorSenseOient = new EventEmitter();

// El try/catch SI que funciona aqui perque emit es SINCRON: l'excepcio es
// llanca dins de la mateixa pila de crides on som.
try {
  emissorSenseOient.emit('error', new Error('La passarella de pagament no respon'));
  console.log("   Aixo no s'imprimeix.");
} catch (error) {
  console.log(`   Capturat: ${error.message}`);
  console.log('   Sense aquest try/catch, el proces hauria mort.');
}

// --- 2. Amb oient registrat ---
console.log('');
console.log('2. Emetre "error" AMB oient registrat');

const emissorAmbOient = new EventEmitter();
emissorAmbOient.on('error', (error) => {
  console.log(`   Oient d'error: ${error.message}`);
});

emissorAmbOient.emit('error', new Error('Fallada transitoria de xarxa'));
console.log('   El proces continua amb normalitat.');

// --- 3. Avis de fuita d'oients ---
console.log('');
console.log('3. Registrar 11 oients del mateix esdeveniment');

const emissorAmbFuita = new EventEmitter();
for (let i = 1; i <= 11; i++) {
  emissorAmbFuita.on('sessio-exhaurida', () => {});
}
console.log(`   Oients registrats: ${emissorAmbFuita.listenerCount('sessio-exhaurida')}`);

// --- 4. Amb el limit pujat ---
console.log('');
console.log('4. Amb setMaxListeners(20), sense avis');

const emissorAmpli = new EventEmitter();
emissorAmpli.setMaxListeners(20);
for (let i = 1; i <= 15; i++) {
  emissorAmpli.on('sessio-exhaurida', () => {});
}
console.log(`   Oients registrats: ${emissorAmpli.listenerCount('sessio-exhaurida')} (sense avis)`);

// --- 5. Diagnostic ---
console.log('');
console.log("5. Diagnostic d'emissorAmbOient");

emissorAmbOient.on('venda-registrada', () => {});
emissorAmbOient.on('venda-registrada', () => {});
emissorAmbOient.on('aforament-baix', () => {});

for (const nom of emissorAmbOient.eventNames()) {
  console.log(`   ${String(nom).padEnd(20)} ${emissorAmbOient.listenerCount(nom)} oient(s)`);
}
1. Emetre "error" sense oient registrat
   Capturat: La passarella de pagament no respon
   Sense aquest try/catch, el proces hauria mort.

2. Emetre "error" AMB oient registrat
   Oient d'error: Fallada transitoria de xarxa
   El proces continua amb normalitat.

3. Registrar 11 oients del mateix esdeveniment
[avis capturat] MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
   Oients registrats: 11

4. Amb setMaxListeners(20), sense avis
   Oients registrats: 15 (sense avis)

5. Diagnostic d'emissorAmbOient
   error                1 oient(s)
   venda-registrada     2 oient(s)
   aforament-baix       1 oient(s)

La resposta al punt 1 és la més instructiva de tot l'exercici: try/catch funciona al voltant d'un emit precisament perquè emit és síncron. És l'excepció a la regla de la lliçó de callbacks. Si EventEmitter despatxés de manera asíncrona, aquest try/catch seria tan inútil com el que envoltava un setTimeout.

Solució 3

// src/laboratori/esperar-exhauriment.js
// Espera el primer exhauriment amb await i un temps limit.

const { once } = require('node:events');
const { GestorDeVendes } = require('../domini/gestor-vendes.js');

const cataleg = [
  {
    id: 'evt-002',
    titol: 'Noche de Monologos',
    sessions: [
      { id: 'ses-002-1', dataHora: '2026-10-10T21:30:00', aforament: 120, venudes: 118, preuCentims: 1800 },
      { id: 'ses-002-2', dataHora: '2026-10-11T21:30:00', aforament: 120, venudes: 45,  preuCentims: 1800 },
      { id: 'ses-002-3', dataHora: '2026-10-17T21:30:00', aforament: 120, venudes: 12,  preuCentims: 1500 }
    ]
  }
];

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

// 2. Espera l'esdeveniment amb temps limit mitjancant AbortController.
async function esperarExhauriment(gestor, limitMs) {
  const controlador = new AbortController();
  const temporitzador = setTimeout(() => controlador.abort(), limitMs);

  try {
    const [dades] = await once(gestor, 'sessio-exhaurida', { signal: controlador.signal });
    return dades;
  } finally {
    // Sense aquest clearTimeout el temporitzador mantindria viu el proces.
    clearTimeout(temporitzador);
  }
}

async function principal() {
  const gestor = new GestorDeVendes(cataleg);

  // Obligatori: sense oient d'error una fallada mataria el proces.
  gestor.on('error', (error) => {
    console.error(`[gestor] ${error.message}`);
  });

  gestor.on('aforament-baix', ({ sessioId, lliuresRestants }) => {
    console.error(`[avis] ${sessioId}: queden ${lliuresRestants} entrades`);
  });

  const inici = Date.now();

  // 3. Vendes aleatories cada 100 ms.
  const simulador = setInterval(() => {
    const sessioId = SESSIONS[Math.floor(Math.random() * SESSIONS.length)];
    const quantitat = 1 + Math.floor(Math.random() * 5);

    try {
      gestor.registrarVenda(sessioId, quantitat, `com-${Date.now()}`);
      console.error(`[venda] ${quantitat} entrades de ${sessioId}`);
    } catch (error) {
      // Aforament insuficient es normal en una simulacio: s'ignora.
      if (error.codi !== 'AFORAMENT_INSUFICIENT') throw error;
    }
  }, 100);

  try {
    const dades = await esperarExhauriment(gestor, 5000);

    // 4. Resultat.
    console.log('');
    console.log(`EXHAURIDA: "${dades.titol}" (${dades.sessioId})`);
    console.log(`  Aforament   : ${dades.aforament}`);
    console.log(`  Recaptacio  : ${(dades.recaptacioCentims / 100).toFixed(2)} EUR`);
    console.log(`  Temps       : ${Date.now() - inici} ms`);
  } catch (error) {
    // 5. Tractament diferenciat de l'AbortError.
    if (error.name === 'AbortError') {
      console.error('');
      console.error("Cap sessio no s'ha exhaurit en 5 segons. S'ha acabat el temps limit.");
      process.exitCode = 1;
      return;
    }
    throw error;
  } finally {
    // 6. Neteja: sense aixo el setInterval mante viu el proces.
    clearInterval(simulador);
  }
}

principal().catch((error) => {
  console.error(`Fallada fatal: ${error.message}`);
  process.exitCode = 1;
});
[venda] 3 entrades de ses-002-3
[venda] 2 entrades de ses-002-2
[avis] ses-002-1: queden 2 entrades
[venda] 0 entrades de ses-002-1
[venda] 4 entrades de ses-002-3
[venda] 2 entrades de ses-002-1

EXHAURIDA: "Noche de Monologos" (ses-002-1)
  Aforament   : 120
  Recaptacio  : 2160.00 EUR
  Temps       : 612 ms

Respostes a les preguntes:

  • Per què events.once() és millor aquí: perquè el flux és «espera un resultat i continua», que és exactament la forma d'una promesa. Amb un gestor.once('sessio-exhaurida', callback) caldria ficar tot el codi posterior dins del callback, perdre try/catch i gestionar el temps límit a mà amb un setTimeout que a més hauria de desregistrar l'oient. events.once() amb AbortSignal resol les quatre coses de cop, i rebutja automàticament si el gestor emet error.
  • Si la sessió s'exhaurís abans de cridar esperarExhauriment, l'esdeveniment s'hauria perdut per sempre: esperarExhauriment es quedaria esperant fins a exhaurir el seu límit de 5 segons. Un esdeveniment no té memòria, a diferència d'una promesa, que guarda el seu resultat per a qui s'hi subscrigui després. És la diferència més important entre tots dos mecanismes i la raó per la qual els emissors no han d'emetre mai al seu constructor: cal donar temps a subscriure-s'hi.

Conclusió

Has incorporat la tercera peça de l'asincronia en Node. El patró observador permet que un objecte anunciï que ha passat alguna cosa sense conèixer els qui hi reaccionen, i EventEmitter és la seva implementació al nucli de Node: on per subscriure's, once per a un sol cop, emit per anunciar, off per desregistrar, i listenerCount/eventNames per diagnosticar.

Has descobert el fet que més sorprèn i que més conseqüències té: els oients s'executen de manera síncrona, en ordre de registre, i emit no torna fins que tots han acabat. D'aquí la regla que un oient ha de ser ràpid i delegar la feina pesada amb setImmediate o amb una operació asíncrona que sempre porti el seu .catch. I d'aquí també l'única excepció al que vas aprendre a la lliçó de callbacks: al voltant d'un emit, try/catch sí que funciona.

Has construït el compromís del Mòdul 1: src/domini/gestor-vendes.js, amb la classe GestorDeVendes extends EventEmitter que emet venda-registrada a cada operació, aforament-baix en creuar el llindar del 10 % lliure —no a cada venda posterior— i sessio-exhaurida quan ja no queden entrades, amb tres oients independents subscrits al mateix esdeveniment i cap de conegut pel gestor. I has fixat la distinció de disseny que importa: els errors d'ús es llancen, els successos del domini s'emeten, i les fallades asíncrones van a l'esdeveniment error.

Saps que error és l'únic esdeveniment que mata el procés si ningú no l'escolta, i per què Node prefereix fallar sorollosament a acumular estat corrupte. Saps que els oients són referències vives i que un MaxListenersExceededWarning gairebé sempre assenyala una fuita real —mai no se silencia pujant el límit sense investigar—. I tens el pont amb la lliçó anterior: events.once() torna una promesa que pots esperar amb await, amb AbortSignal per al temps límit i rebuig automàtic davant d'un error. La regla que ho resumeix tot: una resposta → promesa; moltes notificacions → esdeveniment.

Finalment, has vist que això no és cap racó del llenguatge: process, el servidor HTTP, els sockets, els streams i els processos fills són tots EventEmitter. El que s'ha après aquí és el vocabulari dels deu mòduls restants.

I tot i així, hi ha una cosa que anem arrossegant des del principi. A cada fitxer de laboratori has tornat a copiar l'array cataleg. Has escrit module.exports tres vegades sense que ningú t'hagi explicat què fa exactament. I la promesa de convertir src/cataleg-dades.js en un mòdul reutilitzable i de portar les classes Esdeveniment i Sessio a src/domini/ continua pendent. A la lliçó següent, Mòduls CommonJS i require(), tancarem aquest deute: entendràs l'embolcall que Node afegeix a cada fitxer, per què exports = ... no funciona i module.exports = ... sí, com require troba el que busca, i per què un mòdul és en realitat un singleton.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats