Vas acabar la lliçó anterior amb una piràmide de compra que funcionava, però el cost de la qual era evident: un objecte context per arrossegar l'estat a mà, dos punts de sortida d'error, una compensació niada dins d'un altre callback i la impossibilitat de llançar en paral·lel el que era independent. El diagnòstic va ser clar: el llenguatge no ajudava. try/catch no funcionava, return no servia, finally no existia.

Les promeses tornen tot això. Una promesa és un objecte que representa un valor que encara no està disponible, i que el llenguatge entén: es pot encadenar, compondre, esperar amb await i capturar amb try/catch. Al damunt, async/await afegeix una capa de sintaxi que fa que el codi asíncron es llegeixi exactament igual que el síncron, sense ser-ho.

En aquesta lliçó entendràs què és realment una promesa i els seus tres estats, convertiràs amb util.promisify les funcions de callback que vas escriure ahir, reescriuràs el flux de compra d'Escena Viva fins a deixar-lo pla i llegible, i dominaràs la distinció que separa el principiant del professional: quan esperar en sèrie i quan llançar en paral·lel. Acabaràs amb els patrons que faràs servir cada dia —reintents, temps límit i una funció dormir— i sabent per què un rebuig no gestionat tomba el teu procés en Node modern.

Contingut

  1. Què és una promesa i els seus tres estats
  2. Consumir promeses: .then, .catch i .finally
  3. L'encadenament pla contra la piràmide
  4. Crear promeses: new Promise i util.promisify
  5. async i await: les tres regles
  6. El flux de compra reescrit, costat a costat
  7. Seqüencial contra concurrent: l'error del for amb await
  8. Promise.all, allSettled, race i any
  9. Propagació d'errors i rebuigs no gestionats
  10. await de nivell superior
  11. Patrons útils: dormir, reintents i temps límit
  12. Taula resum: callbacks, promeses i async/await

  1. Què és una promesa i els seus tres estats

Una promesa (Promise) és un objecte que representa el resultat futur d'una operació asíncrona. No és el valor: és un rebut que pots desar, passar a altres funcions i consultar, i que en algun moment es convertirà en un valor o en un error.

Una promesa està sempre en un de tres estats:

stateDiagram-v2
    [*] --> Pendent: es crea la promesa
    Pendent --> Complerta: resolve(valor)
    Pendent --> Rebutjada: reject(error)
    Complerta --> [*]: .then(valor)
    Rebutjada --> [*]: .catch(error)

    note right of Pendent
        L'operació està en curs.
        Encara no hi ha valor.
    end note
    note right of Complerta
        fulfilled: hi ha un valor.
        Estat FINAL.
    end note
    note right of Rebutjada
        rejected: hi ha un error.
        Estat FINAL.
    end note
Estat Nom en anglès Significat
Pendent pending L'operació encara no ha acabat
Complerta fulfilled Va acabar bé i hi ha un valor
Rebutjada rejected Va acabar malament i hi ha un motiu (normalment un Error)

I tres propietats que en defineixen el comportament i que cal gravar a foc:

  1. Una promesa canvia d'estat exactament un cop. De pendent passa a complerta o a rebutjada, i allà s'hi queda per sempre. Es diu que està assentada (settled). Això resol per decret el problema de «el callback s'ha cridat dues vegades»: és impossible.
  2. El resultat és immutable. Un cop complerta amb un valor, aquest valor no canvia.
  3. T'hi pots subscriure quan vulguis, fins i tot tard. Si et subscrius a una promesa ja complerta, la teva funció s'executa igualment (a la cua de microtasques). Amb un callback, si arribes tard, te l'has perdut.

Ho pots veure al REPL, que ja coneixes de la lliçó El REPL de Node.js:

> const p = new Promise((resolve) => setTimeout(() => resolve('llest'), 2000));
> p
Promise { <pending> }          // Abans dels 2 segons

// ... esperes dos segons ...

> p
Promise { 'llest' }            // Ja assentada, i amb el seu valor visible

> Promise.reject(new Error('fallada'))
Promise { <rejected> Error: fallada ... }

  1. Consumir promeses: .then, .catch i .finally

Una promesa es consumeix amb tres mètodes:

// src/laboratori/consumir-promesa.js
const fs = require('node:fs/promises');   // La versio amb promeses de fs

fs.readFile('dades/esdeveniments.json', 'utf8')
  .then((contingut) => {
    // S'executa si la promesa es COMPLEIX. Rep el valor.
    const cataleg = JSON.parse(contingut);
    console.log(`Carregats ${cataleg.length} esdeveniments`);
  })
  .catch((error) => {
    // S'executa si la promesa es REBUTJA en qualsevol punt anterior.
    console.error(`No s'ha pogut carregar el cataleg: ${error.message}`);
  })
  .finally(() => {
    // S'executa SEMPRE, tant si ha anat be com malament. Sense arguments.
    console.error('[cataleg] intent de carrega finalitzat');
  });

Fixa't en node:fs/promises: Node ofereix versions amb promeses dels seus mòduls principals. fs/promises, dns/promises, timers/promises i stream/promises existeixen precisament per no haver de convertir res a mà.

Mètode Quan s'executa Què rep Què torna
.then(fn) En complir-se El valor Una promesa nova
.catch(fn) En rebutjar-se El motiu del rebuig Una promesa nova
.finally(fn) Sempre, en assentar-se Res Una promesa amb el mateix resultat

Aquesta columna de la dreta és la clau de tot el que ve: cada mètode torna una promesa nova, i això és el que permet encadenar.

Dos matisos sobre .finally que sovint s'obliden:

  • No rep arguments, perquè no sap (ni li importa) si hi va haver èxit o error. És per a la neteja: tancar un fitxer, treure un indicador de càrrega, alliberar un recurs.
  • No altera el resultat. Si la promesa es va rebutjar, continua rebutjada després del finally.

  1. L'encadenament pla contra la piràmide

Aquí hi ha el primer gran guany. Recorda la forma de la piràmide:

// CALLBACKS: cada pas dins de l'anterior.
cercarEsdeveniment(esdevenimentId, (error, esdeveniment) => {
  if (error) return console.error(error.message);
  comprovarAforament(sessioId, quantitat, (error, sessio) => {
    if (error) return console.error(error.message);
    crearComanda(usuariId, sessio, quantitat, (error, comanda) => {
      if (error) return console.error(error.message);
      console.log(comanda.id);
    });
  });
});

I ara amb promeses:

// PROMESES: cada pas al MATEIX nivell, amb un unic catch al final.
cercarEsdeveniment(esdevenimentId)
  .then((esdeveniment) => comprovarAforament(sessioId, quantitat))
  .then((sessio) => crearComanda(usuariId, sessio, quantitat))
  .then((comanda) => console.log(comanda.id))
  .catch((error) => console.error(error.message));

La regla que fa això possible és senzilla i potent:

Si dins d'un .then tornes una promesa, la cadena espera que s'assenti abans de continuar.

I la segona regla, igual d'important:

Un rebuig en qualsevol punt de la cadena salta tots els .then següents fins a trobar un .catch.

flowchart LR
    A["cercarEsdeveniment"] -->|"complerta"| B[".then<br/>comprovarAforament"]
    B -->|"complerta"| C[".then<br/>crearComanda"]
    C -->|"complerta"| D[".then<br/>mostrar"]
    D --> E[".catch"]
    A -.->|"rebutjada"| E
    B -.->|"rebutjada"| E
    C -.->|"rebutjada"| E
    D -.->|"rebutjada"| E

Un únic punt de tractament d'errors per a tota la cadena. Compara-ho amb els cinc if (error) return de la lliçó anterior.

L'error clàssic: oblidar el return

// MALAMENT: la cadena NO espera crearComanda.
cercarEsdeveniment(esdevenimentId)
  .then((esdeveniment) => {
    crearComanda(usuariId, esdeveniment, 2);   // Falta el return
  })
  .then((comanda) => {
    console.log(comanda.id);   // TypeError: comanda es undefined
  });

// BE
cercarEsdeveniment(esdevenimentId)
  .then((esdeveniment) => {
    return crearComanda(usuariId, esdeveniment, 2);
  })
  .then((comanda) => {
    console.log(comanda.id);
  });

// BE, en forma curta: la fletxa sense claus torna implicitament.
cercarEsdeveniment(esdevenimentId)
  .then((esdeveniment) => crearComanda(usuariId, esdeveniment, 2))
  .then((comanda) => console.log(comanda.id));

Això és a les promeses el que el return després de l'error era als callbacks: la fallada número u. La forma curta amb fletxa sense claus ho evita d'arrel, i per això es prefereix.

  1. Crear promeses: new Promise i util.promisify

4.1 new Promise

El constructor rep una funció —anomenada executor— amb dos paràmetres: resolve i reject.

// src/utils/dormir.js
// Torna una promesa que es compleix passats els mil·lisegons indicats.

function dormir(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

module.exports = { dormir };
console.log('Abans');
dormir(1000).then(() => console.log('Un segon despres'));

L'executor s'executa immediatament i de manera síncrona en construir la promesa. L'asíncron és el setTimeout de dins, no pas el constructor.

Una versió amb rebuig, aplicada a Escena Viva:

// Simula el cobrament amb la passarella de pagament.
function cobrarComanda(comanda) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (comanda.totalCentims > 20000) {
        const error = new Error(
          `Pagament rebutjat: ${(comanda.totalCentims / 100).toFixed(2)} EUR supera el limit`
        );
        error.codi = 'PAGAMENT_REBUTJAT';
        reject(error);
        return;
      }
      resolve({ ...comanda, estat: 'pagat' });
    }, 40);
  });
}

Regles del constructor:

  • resolve i reject no interrompen la funció. Igual que amb els callbacks, posa-hi un return després.
  • reject sempre amb un objecte Error. reject('alguna cosa ha fallat') és legal i és mala pràctica: en perds la traça.
  • Una excepció llançada dins de l'executor es converteix en un rebuig automàticament. Aquesta sí que la captura la promesa.

Antipatró important: el «constructor antipattern». Si ja tens una promesa, no l'emboliquis en una altra.

// MALAMENT
function carregar() {
  return new Promise((resolve, reject) => {
    fs.readFile('a.json', 'utf8').then(resolve).catch(reject);
  });
}
// BE
function carregar() {
  return fs.readFile('a.json', 'utf8');
}

new Promise és només per embolcallar una cosa que encara no és una promesa: un callback, un esdeveniment, un temporitzador.

4.2 util.promisify: convertir els callbacks d'ahir

Aquí hi ha l'eina que converteix tota la feina de la lliçó anterior. util.promisify pren una funció que segueix la convenció error-first i en torna una altra que torna una promesa.

// src/laboratori/promisificar.js
const { promisify } = require('node:util');

// Les funcions de la llico anterior, amb callback error-first.
// function cercarEsdeveniment(id, callback) { ... }
// function cercarSessio(sessioId, callback) { ... }
// function reservarEntrades(sessioId, quantitat, callback) { ... }

const cercarEsdevenimentP = promisify(cercarEsdeveniment);
const cercarSessioP = promisify(cercarSessio);
const reservarEntradesP = promisify(reservarEntrades);

// I ja es fan servir com a promeses.
cercarEsdevenimentP('evt-002')
  .then((esdeveniment) => console.log(`Trobat: ${esdeveniment.titol}`))
  .catch((error) => console.error(`[${error.codi}] ${error.message}`));

Com funciona per dins, perquè no sigui màgia:

// Versio simplificada del que fa promisify.
function promisificar(funcioAmbCallback) {
  return function (...parametres) {
    return new Promise((resolve, reject) => {
      funcioAmbCallback(...parametres, (error, resultat) => {
        if (error) {
          reject(error);
          return;
        }
        resolve(resultat);
      });
    });
  };
}

Requisits perquè promisify funcioni:

Requisit Si no es compleix
El callback és l'últim paràmetre No funciona
El callback té la signatura (error, resultat) El valor resolt serà incorrecte
El callback es crida un sol cop Les crides extra s'ignoren (la promesa ja està assentada)

Fixa't en aquesta última fila: la promesa et protegeix del bug de «cridar dues vegades el callback». És un d'aquells avantatges silenciosos que s'agraeixen molt.

Si la funció torna diversos valors (no és error-first estàndard), existeix util.promisify.custom per definir la conversió a mà. És poc freqüent; quan ho necessitis, la documentació de node:util ho cobreix.

  1. async i await: les tres regles

async/await no és cap mecanisme nou: és sucre sintàctic sobre promeses. Per sota hi ha exactament les mateixes promeses i la mateixa cua de microtasques. El que canvia és com s'escriu.

Regla 1: una funció async sempre torna una promesa

async function obtenirTitol() {
  return 'Concierto de Otono';   // Tornem una cadena...
}

console.log(obtenirTitol());               // Promise { 'Concierto de Otono' }
obtenirTitol().then((t) => console.log(t)); // Concierto de Otono

Encara que tornis un número, undefined o res, el resultat és una promesa. I si llances una excepció a dins, la promesa es rebutja en lloc de propagar l'excepció:

async function fallar() {
  throw new Error('alguna cosa ha anat malament');
}

fallar().catch((error) => console.error(error.message));   // alguna cosa ha anat malament

Regla 2: await només pausa la funció que el conté

Aquesta és la regla que més es malinterpreta. await no bloqueja el procés ni el fil. Pausa únicament la funció async on apareix; la resta del programa continua funcionant amb normalitat.

// src/laboratori/await-no-bloqueja.js
const { dormir } = require('../utils/dormir.js');

async function tascaLenta() {
  console.log('  [lenta] comenco');
  await dormir(300);
  console.log('  [lenta] acabo');
}

// Un batec que demostra que el proces continua viu.
const batec = setInterval(() => console.log('batec'), 100);

tascaLenta().then(() => clearInterval(batec));

console.log("Aquesta linia s'executa ABANS que tascaLenta acabi");
  [lenta] comenco
Aquesta linia s'executa ABANS que tascaLenta acabi
batec
batec
  [lenta] acabo

Durant els 300 ms d'espera el bucle d'esdeveniments va continuar girant i disparant el temporitzador. Compara-ho amb el bloqueig síncron de la lliçó d'arquitectura, on el batec desapareixia del tot. await cedeix el control; un bucle while el reté.

I await només és vàlid dins d'una funció async (o al nivell superior d'un mòdul ES, apartat 10):

// SyntaxError: await is only valid in async functions
function malament() {
  const esdeveniment = await cercarEsdevenimentP('evt-001');
}

Regla 3: try/catch torna a funcionar

Aquesta és la raó per la qual existeix async/await.

// src/laboratori/try-catch-funciona.js

async function mostrarEsdeveniment(id) {
  try {
    const esdeveniment = await cercarEsdevenimentP(id);
    console.log(`Trobat: ${esdeveniment.titol}`);
  } catch (error) {
    // SI que es captura. L'await converteix el rebuig en una excepcio normal.
    console.error(`[${error.codi}] ${error.message}`);
  } finally {
    // I finally tambe funciona: aqui va la neteja.
    console.error(`[consulta] acabada per a ${id}`);
  }
}

mostrarEsdeveniment('evt-999');
[ESDEVENIMENT_NO_TROBAT] Esdeveniment no trobat: evt-999
[consulta] acabada per a evt-999

Recuperar try/catch/finally no és només comoditat sintàctica: significa que el model d'errors del llenguatge torna a aplicar-se al codi asíncron. Els errors es propaguen cap amunt per la pila de crides async, es capturen on tingui sentit capturar-los, i finally garanteix la neteja. Tota la comptabilitat manual de la lliçó anterior desapareix.

  1. El flux de compra reescrit, costat a costat

Moment de cobrar el premi. Aquest és el mateix flux de l'exercici 3 de la lliçó anterior, ara amb async/await.

Abans, amb callbacks (resumit a la seva estructura):

function comprarEntrades(usuariId, esdevenimentId, sessioId, quantitat, enAcabar) {
  const context = { usuariId, esdevenimentId, sessioId, quantitat, aforamentReservat: false };

  cercarEsdeveniment(esdevenimentId, enTrobarEsdeveniment);

  function enTrobarEsdeveniment(error, esdeveniment) {
    if (error) return fallar('cercar-esdeveniment', error);
    context.esdeveniment = esdeveniment;
    reservarEntrades(sessioId, quantitat, enReservar);
  }
  function enReservar(error, reserva) { /* ... */ }
  function enCercarSessio(error, resultat) { /* ... */ }
  function enCrearComanda(error, comanda) { /* ... */ }
  function enCobrar(error, comandaPagada) { /* ... */ }
  function enEmetre(error, entrades) { /* ... */ }
  function fallar(pas, error) { /* ... */ }
  function compensarIFallar(pas, error) {
    alliberarAforament(sessioId, quantitat, (errorAlliberar) => { /* ... */ });
  }
}

Després, amb async/await:

// src/laboratori/compra-async.js
// El mateix flux de compra, amb promeses. 30 linies en lloc de 70.

async function comprarEntrades(usuariId, esdevenimentId, sessioId, quantitat) {
  const esdeveniment = await cercarEsdevenimentP(esdevenimentId);

  // A partir d'aqui hi ha aforament reservat que potser caldra tornar.
  const reserva = await reservarEntradesP(sessioId, quantitat);

  try {
    const { sessio } = await cercarSessioP(sessioId);
    const comanda = await crearComandaP(usuariId, sessio, quantitat);
    const comandaPagada = await cobrarComanda(comanda);
    const entrades = await emetreEntradesP(comandaPagada);

    return { esdeveniment, sessio, comanda: comandaPagada, entrades };
  } catch (error) {
    // Compensacio: alliberem l'aforament que haviem reservat.
    const lliures = await alliberarAforamentP(sessioId, quantitat);
    console.error(`[compensacio] aforament alliberat a ${sessioId}, en queden ${lliures} lliures`);

    // I el rellancem: qui ens ha cridat decideix que fer.
    throw error;
  }
}

I el seu ús:

async function principal() {
  try {
    const compra = await comprarEntrades('asis-001', 'evt-002', 'ses-002-2', 3);

    console.log(`Comanda ${compra.comanda.id} - ${compra.comanda.estat}`);
    console.log(`  Esdeveniment: ${compra.esdeveniment.titol}`);
    console.log(`  Import      : ${(compra.comanda.totalCentims / 100).toFixed(2)} EUR`);
    for (const entrada of compra.entrades) {
      console.log(`  ${entrada.codi}  ${entrada.estat}`);
    }
  } catch (error) {
    console.error(`[${error.codi || 'ERROR'}] ${error.message}`);
    process.exitCode = 1;
  }
}

principal();

Compara punt per punt:

Aspecte Callbacks async/await
Línies del flux ~70 ~30
Nivells d'indentació 2 (amb la tècnica d'aplanament) 1
Objecte context per a l'estat Necessari Innecessari: són variables locals normals
Punts de tractament d'errors 2 (fallar i compensarIFallar) 1 (catch)
Compensació Callback niat amb el seu propi error 2 línies dins del catch
Tornar el resultat Impossible; cal passar un callback return normal
Ordre de lectura Saltant entre funcions De dalt a baix

I fixa't en el detall més elegant: esdeveniment, reserva, sessio, comanda i entrades són variables locals corrents, visibles a tot el cos de la funció. Allò d'arrossegar el context per cinc callbacks ha desaparegut, i no pas perquè hàgim estat més llestos: perquè el llenguatge ara entén l'espera.

  1. Seqüencial contra concurrent: l'error del for amb await

async/await és tan còmode que indueix a un error de rendiment molt freqüent i molt car. Mira'l:

// src/laboratori/sequencial-vs-parallel.js
// MALAMENT: els tres esdeveniments son independents, pero es carreguen en serie.

async function carregarEsdevenimentsEnSerie(ids) {
  const esdeveniments = [];
  for (const id of ids) {
    const esdeveniment = await cercarEsdevenimentP(id);   // Espera que acabi l'anterior
    esdeveniments.push(esdeveniment);
  }
  return esdeveniments;
}

const inici = Date.now();
carregarEsdevenimentsEnSerie(['evt-001', 'evt-002', 'evt-003']).then((esdeveniments) => {
  console.log(`En serie: ${esdeveniments.length} esdeveniments en ${Date.now() - inici} ms`);
});
En serie: 3 esdeveniments en 124 ms

Amb 40 ms de latència per consulta, tres consultes en sèrie costen 120 ms. Però els tres esdeveniments no depenen l'un de l'altre: no hi ha cap raó per esperar que arribi el primer abans de demanar el segon.

// BE: les tres peticions surten alhora.
async function carregarEsdevenimentsEnParallel(ids) {
  // map torna un array de PROMESES: les tres operacions ja han comencat.
  const promeses = ids.map((id) => cercarEsdevenimentP(id));

  // Promise.all espera que TOTES es compleixin.
  return Promise.all(promeses);
}

const inici2 = Date.now();
carregarEsdevenimentsEnParallel(['evt-001', 'evt-002', 'evt-003']).then((esdeveniments) => {
  console.log(`En parallel: ${esdeveniments.length} esdeveniments en ${Date.now() - inici2} ms`);
});
En parallel: 3 esdeveniments en 42 ms

Tres vegades més ràpid, i amb tres esdeveniments. Amb trenta, la diferència seria d'1,2 segons contra 40 mil·lisegons.

gantt
    dateFormat SSS
    axisFormat %L ms
    title Tres consultes de 40 ms
    section En serie (await en bucle)
    evt-001 :a1, 000, 40ms
    evt-002 :a2, after a1, 40ms
    evt-003 :a3, after a2, 40ms
    section En parallel (Promise.all)
    evt-001 :b1, 000, 40ms
    evt-002 :b2, 000, 40ms
    evt-003 :b3, 000, 40ms

La clau per entendre-ho: una promesa comença a treballar en el moment en què es crea, no pas quan se li fa await. El map crea les tres promeses de cop, així que les tres operacions arrenquen alhora; Promise.all només s'encarrega d'esperar.

Quan cadascuna

Situació Què fer servir
El pas B necessita el resultat del pas A await seqüencial. No hi ha alternativa
Els passos són independents entre si Promise.all
Són independents però el destí no aguanta la càrrega (una API amb límit) Paral·lelisme acotat: lots de N
Vols el primer que respongui Promise.race o Promise.any

Al flux de compra de l'apartat 6, els await sí que han de ser seqüencials: no pots emetre entrades d'una comanda que no has cobrat. En canvi, carregar el catàleg complet o consultar tres organitzadors diferents és feina paral·lela.

Paral·lelisme acotat

Llançar 3.000 peticions alhora amb Promise.all no és «més ràpid»: és una manera de tombar la base de dades o que l'API externa et bloquegi. El patró correcte és processar en lots:

// src/utils/en-lots.js
// Executa una tasca sobre molts elements, com a maxim N alhora.

async function enLots(elements, midaLot, tasca) {
  const resultats = [];

  for (let i = 0; i < elements.length; i += midaLot) {
    const lot = elements.slice(i, i + midaLot);
    // Dins del lot, en parallel. Entre lots, en serie.
    const resultatsLot = await Promise.all(lot.map(tasca));
    resultats.push(...resultatsLot);
  }

  return resultats;
}

module.exports = { enLots };
// Consultar 3000 sessions de 20 en 20.
const ocupacions = await enLots(idsSessions, 20, (id) => consultarOcupacio(id));

  1. Promise.all, allSettled, race i any

Els quatre combinadors de promeses. Triar malament és una font habitual d'errors subtils.

Mètode Es compleix quan… Es rebutja quan… Torna
Promise.all Totes es compleixen Alguna es rebutja (la primera) Array de valors, en l'ordre d'entrada
Promise.allSettled Totes s'assenten (no es rebutja mai) Mai Array de { status, value } o { status, reason }
Promise.race La primera a assentar-se es compleix La primera a assentar-se es rebutja El valor o error de la primera
Promise.any La primera a complir-se Totes es rebutgen El valor de la primera que ha funcionat

Promise.all: tot o res

const [almendra, boveda, ribera] = await Promise.all([
  cercarEsdevenimentP('evt-001'),
  cercarEsdevenimentP('evt-002'),
  cercarEsdevenimentP('evt-003')
]);

La desestructuració funciona perquè l'ordre del resultat és el d'entrada, no pas el de finalització.

El seu comportament davant de la fallada és el que cal entendre bé:

try {
  const esdeveniments = await Promise.all([
    cercarEsdevenimentP('evt-001'),
    cercarEsdevenimentP('evt-999'),   // No existeix: es rebutja
    cercarEsdevenimentP('evt-003')
  ]);
} catch (error) {
  console.error(error.message);   // Esdeveniment no trobat: evt-999
  // I d'evt-001 i evt-003 no en sabem res, encara que probablement van anar be.
}

Promise.all és «tot o res»: al primer rebuig, la promesa combinada es rebutja i perds els resultats de les altres. A més, les altres operacions no es cancel·len: continuen executant-se, simplement ja no interessen a ningú.

Fes-lo servir quan necessites tots els resultats per continuar. Si en falta un, la feina no té sentit.

Promise.allSettled: ho vull saber tot

// src/laboratori/informe-resilient.js
// Un informe que no ha de caure perque falli un esdeveniment.

const resultats = await Promise.allSettled([
  cercarEsdevenimentP('evt-001'),
  cercarEsdevenimentP('evt-999'),
  cercarEsdevenimentP('evt-003')
]);

const trobats = [];
const fallits = [];

for (const resultat of resultats) {
  if (resultat.status === 'fulfilled') {
    trobats.push(resultat.value.titol);
  } else {
    fallits.push(resultat.reason.message);
  }
}

console.log(`Trobats (${trobats.length}): ${trobats.join(', ')}`);
console.error(`Fallits (${fallits.length}): ${fallits.join(' | ')}`);
Trobats (2): Concierto de Otono, Festival de Jazz de Primavera
Fallits (1): Esdeveniment no trobat: evt-999

allSettled no es rebutja mai. És l'elecció correcta per a informes, sincronitzacions i qualsevol procés per lots on una fallada parcial no ha d'invalidar la resta. A Escena Viva: enviar 500 correus de confirmació amb Promise.all significa que un correu rebotat cancel·la l'informe dels altres 499; amb allSettled, saps exactament quins es van enviar i quins no.

Promise.race: el primer que arribi, per bé o per mal

// Temps limit: o respon la passarella, o es rebutja als 3 segons.
const comandaPagada = await Promise.race([
  cobrarComanda(comanda),
  rebutjarDespresDe(3000, 'La passarella de pagament no respon')
]);

El seu ús principal és exactament aquest: imposar un temps límit. El desenvolupem a l'apartat 11.

Compte amb un parany: race s'assenta amb la primera promesa que s'assenti, encara que sigui un rebuig. Si vols «el primer que funcioni», ignorant fallades, necessites any.

Promise.any: el primer que funcioni

// Consultar el preu a tres proveidors; ens val el primer que respongui be.
try {
  const tipusCanvi = await Promise.any([
    consultarProveidorA(),
    consultarProveidorB(),
    consultarProveidorC()
  ]);
  console.log(`Tipus de canvi obtingut: ${tipusCanvi}`);
} catch (error) {
  // AggregateError: conte TOTS els errors a error.errors
  console.error(`Cap proveidor no ha respost (${error.errors.length} fallades)`);
  for (const fallada of error.errors) {
    console.error(`  - ${fallada.message}`);
  }
}

Promise.any només es rebutja si totes fallen, i ho fa amb un AggregateError que conté l'array complet d'errors a .errors. És el patró de redundància: diverses rèpliques o diversos proveïdors per al mateix.

Guia ràpida de decisió

Vull… Faig servir
Carregar les dades que necessito per respondre, i si en falta una no puc respondre Promise.all
Processar un lot on les fallades parcials són acceptables i cal reportar-les Promise.allSettled
Posar un temps límit a una operació Promise.race
Consultar diverses fonts redundants i quedar-me amb la primera que funcioni Promise.any

  1. Propagació d'errors i rebuigs no gestionats

9.1 Els errors pugen per la pila async

async function nivell3() {
  throw new Error('fallada al nivell mes profund');
}

async function nivell2() {
  await nivell3();          // El rebuig es propaga cap amunt
  console.log("Aixo no s'executa");
}

async function nivell1() {
  try {
    await nivell2();
  } catch (error) {
    console.error(`Capturat a nivell1: ${error.message}`);
  }
}

nivell1();   // Capturat a nivell1: fallada al nivell mes profund

Exactament igual que amb codi síncron. És la propietat que fa que async/await sigui tan còmode: captures els errors on tens context per decidir què fer, no pas a cada pas intermedi.

9.2 El rebuig no gestionat tomba el procés

Si una promesa es rebutja i ningú no ha registrat un .catch ni l'ha esperada dins d'un try, es produeix un rebuig no gestionat (unhandled rejection).

// src/laboratori/rebuig-no-gestionat.js

async function cobrar() {
  throw new Error('La passarella ha tornat un error 500');
}

cobrar();   // Es crida, s'ignora el resultat. NINGU no captura el rebuig.

console.log("L'script continua...");
L'script continua...

node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);
            ^
Error: La passarella ha tornat un error 500
    ...
[el proces mor amb codi 1]

Des de Node.js 15, un rebuig no gestionat acaba el procés. Abans només imprimia un avís, i moltes aplicacions vivien amb desenes de rebuigs silenciosos que amagaven errors reals. El canvi va ser deliberat: un rebuig sense gestionar és un error de programació, exactament igual que una excepció sense capturar.

Els tres descuits que el provoquen:

// 1. Cridar una funcio async sense await ni .catch
processarComanda(comanda);                       // MALAMENT
await processarComanda(comanda);                 // BE
processarComanda(comanda).catch(registrarError); // BE, si no vols esperar

// 2. Oblidar el catch en una cadena
cercarEsdevenimentP(id).then((e) => console.log(e.titol));            // MALAMENT
cercarEsdevenimentP(id).then((e) => console.log(e.titol)).catch(log); // BE

// 3. La funcio principal sense protegir
async function principal() { /* ... */ }
principal();                                   // MALAMENT
principal().catch((error) => {                 // BE
  console.error(`Fallada fatal: ${error.message}`);
  process.exitCode = 1;
});

Com a xarxa de seguretat per al registre —no com a gestió d'errors— pots escoltar l'esdeveniment del procés:

// src/utils/xarxa-de-seguretat.js
// Registra qualsevol rebuig no gestionat abans que el proces mori.

process.on('unhandledRejection', (motiu, promesa) => {
  console.error('REBUIG NO GESTIONAT. Aixo es una fallada de programacio.');
  console.error(motiu instanceof Error ? motiu.stack : motiu);
  process.exitCode = 1;
});

Això serveix per assabentar-te del problema i registrar-lo en producció, no per continuar com si res. Ho formalitzarem al Mòdul 11.

  1. await de nivell superior

En un mòdul CommonJS —el que estem fent servir i el que estudiarem a Mòduls CommonJS i require()— await només pot aparèixer dins d'una funció async. Per això hem hagut d'embolcallar-ho tot en una funció principal().

En un mòdul ES, await funciona directament al nivell superior del fitxer:

// src/laboratori/carrega.mjs   (fixa't en l'extensio .mjs)
import { readFile } from 'node:fs/promises';

// await directament, sense embolcallar-ho en cap funcio.
const contingut = await readFile('dades/esdeveniments.json', 'utf8');
const cataleg = JSON.parse(contingut);

console.log(`Carregats ${cataleg.length} esdeveniments`);

És un dels avantatges exclusius dels mòduls ES, i és especialment còmode per a scripts, per a la inicialització de configuració i per al REPL (que, com vas veure al Mòdul 1, també l'admet).

Ho veurem en detall —juntament amb com activar-lo, les seves regles i la seva interoperabilitat amb CommonJS— a la lliçó Mòduls ES i Interoperabilitat.

  1. Patrons útils: dormir, reintents i temps límit

Tres utilitats que escriuràs un cop i faràs servir sempre.

11.1 dormir(ms)

// src/utils/dormir.js
function dormir(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

module.exports = { dormir };

Node porta una versió nativa des de la versió 15, a timers/promises:

const { setTimeout: dormir } = require('node:timers/promises');

await dormir(1000);
console.log('Un segon despres');

Fes servir la nativa quan puguis: admet a més un senyal de cancel·lació (AbortSignal).

11.2 Reintents amb espera creixent

Les operacions de xarxa fallen de manera transitòria: un pic de latència, un reinici del servei, un límit de peticions momentani. Reintentar és sovint la resposta correcta.

// src/utils/reintentar.js
// Reintenta una operacio asincrona amb espera exponencial.

const { setTimeout: dormir } = require('node:timers/promises');

async function reintentar(operacio, opcions = {}) {
  const {
    intents = 3,
    esperaInicialMs = 200,
    factor = 2,
    esReintentable = () => true
  } = opcions;

  let ultimError;

  for (let intent = 1; intent <= intents; intent++) {
    try {
      return await operacio(intent);
    } catch (error) {
      ultimError = error;

      // Un error de negoci no es reintenta: reintentar un aforament
      // insuficient no ho arreglara.
      if (!esReintentable(error) || intent === intents) {
        throw error;
      }

      // Espera exponencial: 200, 400, 800 ms...
      const espera = esperaInicialMs * factor ** (intent - 1);
      console.error(
        `[reintent] intent ${intent}/${intents} fallit (${error.message}). ` +
        `Es reintenta en ${espera} ms.`
      );
      await dormir(espera);
    }
  }

  throw ultimError;
}

module.exports = { reintentar };
// Us a Escena Viva: cobrar reintentant nomes les fallades de xarxa.
const CODIS_REINTENTABLES = new Set(['ETIMEDOUT', 'ECONNRESET', 'PASSARELLA_NO_DISPONIBLE']);

const comandaPagada = await reintentar(
  () => cobrarComanda(comanda),
  {
    intents: 4,
    esperaInicialMs: 250,
    esReintentable: (error) => CODIS_REINTENTABLES.has(error.codi)
  }
);

Fixa't en esReintentable. És la part més important del patró, i la que gairebé tothom omet: reintentar un PAGAMENT_REBUTJAT quatre vegades no només és inútil, sinó que pot duplicar càrrecs. Només es reintenta el que és transitori.

En producció s'hi afegeix a més jitter: una petita variació aleatòria en l'espera, perquè mil clients que han fallat alhora no reintentin tots al mateix mil·lisegon.

11.3 Temps límit amb Promise.race

Una operació asíncrona que no respon mai és pitjor que una que falla: deixa recursos ocupats indefinidament.

// src/utils/amb-limit-de-temps.js
// Embolcalla una promesa amb un temps maxim d'espera.

function ambLimitDeTemps(promesa, limitMs, missatge = "Temps d'espera exhaurit") {
  let temporitzador;

  const limit = new Promise((_, reject) => {
    temporitzador = setTimeout(() => {
      const error = new Error(`${missatge} (${limitMs} ms)`);
      error.codi = 'TEMPS_EXHAURIT';
      reject(error);
    }, limitMs);
  });

  // El primer que s'assenti guanya.
  return Promise.race([promesa, limit]).finally(() => {
    // Netegem el temporitzador per no retenir el proces viu.
    clearTimeout(temporitzador);
  });
}

module.exports = { ambLimitDeTemps };
try {
  const comandaPagada = await ambLimitDeTemps(
    cobrarComanda(comanda),
    3000,
    'La passarella de pagament no respon'
  );
  console.log(`Cobrada: ${comandaPagada.id}`);
} catch (error) {
  if (error.codi === 'TEMPS_EXHAURIT') {
    console.error("Reintentar mes tard o avisar l'usuari.");
  }
  throw error;
}

Aquest .finally(() => clearTimeout(temporitzador)) no és cap detall menor: sense ell, el temporitzador manté viu el procés fins que venci, encara que l'operació hagi acabat en 20 ms. És justament el mecanisme de compte de referències que vas veure a la lliçó d'arquitectura.

I un advertiment honest: el temps límit no cancel·la l'operació subjacent. La petició HTTP continua en marxa; simplement deixes d'esperar-la. Per cancel·lar de debò cal AbortController, que les APIs modernes de Node (fetch, fs/promises, timers/promises) sí que accepten.

  1. Taula resum: callbacks, promeses i async/await

Criteri Callbacks Promeses (.then) async/await
Sintaxi Funcions niades Cadena de mètodes Lineal, com codi síncron
Llegibilitat de fluxos llargs Dolenta (piràmide) Bona Excel·lent
Gestió d'errors if (error) a cada pas Un .catch per cadena try/catch/finally natiu
finally / neteja Manual .finally() finally natiu
Tornar un valor Impossible Sí (una promesa) Sí, amb return
Execució en paral·lel Manual i propensa a errors Promise.all Promise.all + await
Es pot cridar dues vegades Sí, és un bug freqüent Impossible Impossible
Variables entre passos Tancaments niats o context Tancaments o encadenat Variables locals normals
Depuració i traces Traces pobres Millors Traces async completes
Múltiples resultats al llarg del temps Sí No (s'assenta un cop) No
Cost de rendiment El menor Cua de microtasques Com les promeses
Suport en Node Sempre Des de Node 0.12 Des de Node 7.6
Quan fer-lo servir Esdeveniments, streams, APIs antigues Composició puntual, Promise.all Tota la resta

La conclusió pràctica per a la resta del curs:

Escriu async/await per defecte. Fes servir .then quan componguis promeses sense necessitat d'esperar-les (Promise.all, .catch en una crida que no esperes). Fes servir callbacks quan l'API t'hi obligui o quan el resultat sigui repetit en el temps — és a dir, quan estiguis davant d'un esdeveniment.

Errors Comuns i Consells

Error 1: oblidar el return dins d'un .then. La cadena no espera i el pas següent rep undefined. Fes servir fletxes sense claus per evitar-ho.

Error 2: await dins d'un for quan les tasques són independents. Multipliques el temps pel nombre d'elements. Promise.all amb map.

Error 3: creure que await bloqueja el procés. Només pausa la funció que el conté. El bucle d'esdeveniments continua girant.

Error 4: cridar una funció async sense await ni .catch. Rebuig no gestionat, i des de Node 15 això mata el procés.

Error 5: Promise.all quan el correcte és allSettled. Una fallada parcial cancel·la tot el lot i perds els resultats bons.

Error 6: forEach amb funcions async.

// NO espera res: forEach ignora les promeses que torna el callback.
ids.forEach(async (id) => { await processar(id); });
console.log('Acabat');   // Mentida: no ha acabat res

// Correcte:
await Promise.all(ids.map((id) => processar(id)));

Error 7: embolcallar una promesa en new Promise. El constructor antipattern. Si ja és una promesa, torna-la tal qual.

Error 8: reject('text') en lloc de reject(new Error('text')). Perds la traça de pila i trenques la convenció que tot l'ecosistema espera.

Error 9: reintentar errors que no són transitoris. Reintentar quatre vegades un pagament rebutjat pot acabar en càrrecs duplicats.

Consell 1: protegeix sempre la teva funció principal. principal().catch(...) a l'última línia del fitxer, sense excepció.

Consell 2: pregunta't a cada await: «això necessita el resultat de l'anterior?» Si la resposta és no, hi ha una oportunitat de Promise.all.

Consell 3: fes servir node:fs/promises i node:timers/promises directament. No promisifiquis allò que Node ja et dona fet.

Consell 4: mantén els errors amb codi. La convenció de la lliçó anterior continua valent, i amb promeses és encara més útil, perquè un sol catch rep errors de passos molt diferents i necessita distingir-los.

Exercicis

Exercici 1: convertir la capa de dades i mesurar la diferència

Escriu src/laboratori/cataleg-promeses.js que:

  1. Prengui les funcions cercarEsdeveniment, cercarSessio i reservarEntrades de la lliçó anterior i les converteixi amb util.promisify.
  2. Implementi carregarCatalegSerie(ids) amb await en un bucle for.
  3. Implementi carregarCatalegParallel(ids) amb Promise.all.
  4. Executi les dues amb els tres esdeveniments, mesuri els temps amb Date.now() i mostri una taula comparativa amb console.table que inclogui el temps de cadascuna i el factor de millora.
  5. Afegeixi una tercera variant carregarCatalegResilient(ids) amb Promise.allSettled que funcioni fins i tot si se li passa evt-999, informant de les fallades per stderr.

Respon per escrit: quin factor de millora obtens amb 3 esdeveniments? I amb 10? Per què el factor no creix indefinidament en un cas real?

Exercici 2: la compra amb temps límit i reintents

Partint del flux comprarEntrades de l'apartat 6, escriu src/laboratori/compra-robusta.js que hi afegeixi:

  1. Un temps límit de 2 segons al cobrament, fent servir ambLimitDeTemps.
  2. Reintents del cobrament: fins a 3 intents amb espera exponencial des de 200 ms, només per a errors amb codi TEMPS_EXHAURIT, ETIMEDOUT o PASSARELLA_NO_DISPONIBLE.
  3. Una passarella simulada cobrarComanda(comanda) que:
    • Rebutgi amb PAGAMENT_REBUTJAT si el total supera 20000 cèntims (no reintentable).
    • Falli amb PASSARELLA_NO_DISPONIBLE les dues primeres vegades que se la cridi i funcioni a la tercera (reintentable).
  4. Compensació correcta amb try/catch: si alguna cosa falla després de reservar l'aforament, s'allibera.
  5. Registre per stderr de cada intent, i resultat final per stdout.

Comprova els dos camins: un que acaba funcionant després de dos reintents i un que es rebutja sense reintentar.

Exercici 3: el tauler d'ocupació, en paral·lel i per lots

Escriu src/laboratori/panell-ocupacio.js que generi el tauler d'ocupació d'Escena Viva:

  1. Una funció consultarOcupacio(sessioId) que torni una promesa amb { sessioId, sala, percentatge, recaptacioCentims } després d'una latència de 30 ms.
  2. generarPanell(idsSessions, midaLot) que faci servir l'ajudant enLots de l'apartat 7 per consultar totes les sessions amb un màxim de midaLot en vol simultàniament.
  3. Agregació per sala: total de sessions, ocupació mitjana i recaptació en euros.
  4. Mesura del retard del bucle d'esdeveniments durant la generació, amb monitorEventLoopDelay de la lliçó anterior, per demostrar que la versió amb promeses no bloqueja encara que processi 300 sessions.
  5. Comparativa de temps amb mides de lot 1, 10, 50 i 300.

Respon: per què el lot de 300 és el més ràpid aquí i, tot i així, no seria la millor elecció contra una base de dades real?

Solucions

Solució 1

// src/laboratori/cataleg-promeses.js
// Compara la carrega sequencial, parallela i resilient del cataleg.

const { promisify } = require('node:util');

// cercarEsdeveniment ve de la llico anterior (callback error-first).
const cercarEsdevenimentP = promisify(cercarEsdeveniment);

// 2. En serie: cada peticio espera l'anterior.
async function carregarCatalegSerie(ids) {
  const esdeveniments = [];
  for (const id of ids) {
    esdeveniments.push(await cercarEsdevenimentP(id));
  }
  return esdeveniments;
}

// 3. En parallel: totes les peticions arrenquen alhora.
async function carregarCatalegParallel(ids) {
  return Promise.all(ids.map((id) => cercarEsdevenimentP(id)));
}

// 5. Resilient: les fallades parcials no invaliden la resta.
async function carregarCatalegResilient(ids) {
  const resultats = await Promise.allSettled(ids.map((id) => cercarEsdevenimentP(id)));

  const esdeveniments = [];
  const fallades = [];

  resultats.forEach((resultat, index) => {
    if (resultat.status === 'fulfilled') {
      esdeveniments.push(resultat.value);
    } else {
      fallades.push({ id: ids[index], motiu: resultat.reason.message });
    }
  });

  for (const fallada of fallades) {
    console.error(`[cataleg] no s'ha pogut carregar ${fallada.id}: ${fallada.motiu}`);
  }

  return { esdeveniments, fallades };
}

// --- Mesura ---
async function mesurar(etiqueta, funcio, ids) {
  const inici = Date.now();
  const resultat = await funcio(ids);
  const ms = Date.now() - inici;
  const quants = Array.isArray(resultat) ? resultat.length : resultat.esdeveniments.length;
  return { etiqueta, esdeveniments: quants, ms };
}

async function principal() {
  const ids = ['evt-001', 'evt-002', 'evt-003'];

  const serie = await mesurar('serie', carregarCatalegSerie, ids);
  const parallel = await mesurar('parallel', carregarCatalegParallel, ids);

  console.table([
    serie,
    parallel,
    { etiqueta: 'millora', esdeveniments: '-', ms: `x${(serie.ms / parallel.ms).toFixed(1)}` }
  ]);

  console.error('');
  console.error('--- Variant resilient amb un id inexistent ---');
  const resilient = await carregarCatalegResilient([...ids, 'evt-999']);
  console.log(
    `Resilient: ${resilient.esdeveniments.length} carregats, ${resilient.fallades.length} fallits`
  );
}

principal().catch((error) => {
  console.error(`Fallada fatal: ${error.message}`);
  process.exitCode = 1;
});
┌─────────┬────────────┬───────────────┬────────┐
│ (index) │ etiqueta   │ esdeveniments │ ms     │
├─────────┼────────────┼───────────────┼────────┤
│ 0       │ 'serie'    │ 3             │ 124    │
│ 1       │ 'parallel' │ 3             │ 42     │
│ 2       │ 'millora'  │ '-'           │ 'x3.0' │
└─────────┴────────────┴───────────────┴────────┘

--- Variant resilient amb un id inexistent ---
[cataleg] no s'ha pogut carregar evt-999: Esdeveniment no trobat: evt-999
Resilient: 3 carregats, 1 fallits

Respostes:

  • Amb 3 esdeveniments: factor ~3. Amb 10 esdeveniments: factor ~10 (400 ms contra 40 ms). En aquest laboratori la millora és lineal perquè setTimeout no consumeix cap recurs compartit.
  • En un cas real el factor no creix indefinidament per tres motius: el destí té un límit (una base de dades amb 20 connexions no atén 500 consultes simultànies més ràpid que 20 alhora), la xarxa té una amplada de banda finita, i el mateix Node té el thread pool de 4 fils per a les operacions que hi passen. A partir d'un cert punt, més concurrència només afegeix cua. D'aquí el patró de paral·lelisme acotat per lots.

Solució 2

// src/laboratori/compra-robusta.js
// Flux de compra amb temps limit, reintents selectius i compensacio.

const { setTimeout: dormir } = require('node:timers/promises');

const CODIS_REINTENTABLES = new Set([
  'TEMPS_EXHAURIT', 'ETIMEDOUT', 'PASSARELLA_NO_DISPONIBLE'
]);

// --- Utilitats ---

function ambLimitDeTemps(promesa, limitMs, missatge = "Temps d'espera exhaurit") {
  let temporitzador;
  const limit = new Promise((_, reject) => {
    temporitzador = setTimeout(() => {
      const error = new Error(`${missatge} (${limitMs} ms)`);
      error.codi = 'TEMPS_EXHAURIT';
      reject(error);
    }, limitMs);
  });
  return Promise.race([promesa, limit]).finally(() => clearTimeout(temporitzador));
}

async function reintentar(operacio, { intents = 3, esperaInicialMs = 200, factor = 2 } = {}) {
  for (let intent = 1; intent <= intents; intent++) {
    try {
      return await operacio(intent);
    } catch (error) {
      const reintentable = CODIS_REINTENTABLES.has(error.codi);

      if (!reintentable) {
        console.error(`[reintent] "${error.codi}" no es reintentable. Ho deixo.`);
        throw error;
      }
      if (intent === intents) {
        console.error(`[reintent] exhaurits els ${intents} intents.`);
        throw error;
      }

      const espera = esperaInicialMs * factor ** (intent - 1);
      console.error(
        `[reintent] intent ${intent}/${intents} fallit (${error.message}). ` +
        `Reintent en ${espera} ms.`
      );
      await dormir(espera);
    }
  }
}

// --- Passarella simulada ---

let cridesAPassarella = 0;

function cobrarComanda(comanda) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Error de negoci: NO reintentable.
      if (comanda.totalCentims > 20000) {
        const error = new Error(
          `Pagament rebutjat: ${(comanda.totalCentims / 100).toFixed(2)} EUR supera el limit`
        );
        error.codi = 'PAGAMENT_REBUTJAT';
        reject(error);
        return;
      }

      // Error transitori: les dues primeres crides fallen.
      cridesAPassarella++;
      if (cridesAPassarella <= 2) {
        const error = new Error('La passarella no esta disponible');
        error.codi = 'PASSARELLA_NO_DISPONIBLE';
        reject(error);
        return;
      }

      resolve({ ...comanda, estat: 'pagat' });
    }, 40);
  });
}

// --- Flux de compra ---

async function comprarEntrades(usuariId, esdevenimentId, sessioId, quantitat) {
  const esdeveniment = await cercarEsdevenimentP(esdevenimentId);
  await reservarEntradesP(sessioId, quantitat);

  try {
    const { sessio } = await cercarSessioP(sessioId);
    const comanda = await crearComandaP(usuariId, sessio, quantitat);

    // Reintents per fora, temps limit per dins: cada intent
    // te els seus propis 2 segons.
    const comandaPagada = await reintentar(
      () => ambLimitDeTemps(cobrarComanda(comanda), 2000, 'La passarella no respon'),
      { intents: 3, esperaInicialMs: 200 }
    );

    const entrades = await emetreEntradesP(comandaPagada);
    return { esdeveniment, sessio, comanda: comandaPagada, entrades };
  } catch (error) {
    const lliures = await alliberarAforamentP(sessioId, quantitat);
    console.error(`[compensacio] aforament alliberat a ${sessioId}, en queden ${lliures} lliures`);
    throw error;
  }
}

// --- Proves ---

async function principal() {
  // Cami 1: 3 x 18,00 = 54,00 EUR. Falla dues vegades i funciona a la tercera.
  console.error('--- Compra que acaba funcionant despres de dos reintents ---');
  const compra = await comprarEntrades('asis-001', 'evt-002', 'ses-002-2', 3);
  console.log(`Comanda ${compra.comanda.id} - ${compra.comanda.estat}`);
  console.log(`  Import  : ${(compra.comanda.totalCentims / 100).toFixed(2)} EUR`);
  console.log(`  Entrades: ${compra.entrades.map((e) => e.codi).join(', ')}`);

  // Cami 2: 5 x 42,00 = 210,00 EUR. Rebuig no reintentable.
  console.error('');
  console.error('--- Compra rebutjada sense reintents ---');
  try {
    await comprarEntrades('asis-002', 'evt-003', 'ses-003-2', 5);
  } catch (error) {
    console.error(`Resultat: [${error.codi}] ${error.message}`);
  }
}

principal().catch((error) => {
  console.error(`Fallada fatal: ${error.message}`);
  process.exitCode = 1;
});
--- Compra que acaba funcionant despres de dos reintents ---
[reintent] intent 1/3 fallit (La passarella no esta disponible). Reintent en 200 ms.
[reintent] intent 2/3 fallit (La passarella no esta disponible). Reintent en 400 ms.
Comanda com-001 - pagat
  Import  : 54.00 EUR
  Entrades: EV-2026-000001, EV-2026-000002, EV-2026-000003

--- Compra rebutjada sense reintents ---
[reintent] "PAGAMENT_REBUTJAT" no es reintentable. Ho deixo.
[compensacio] aforament alliberat a ses-003-2, en queden 180 lliures
Resultat: [PAGAMENT_REBUTJAT] Pagament rebutjat: 210.00 EUR supera el limit

Dues decisions de disseny que convé subratllar:

  1. El temps límit va dins del reintent, no pas fora. Així cada intent disposa dels seus propis 2 segons. A l'inrevés, el límit global tallaria la cadena de reintents a mitges.
  2. PAGAMENT_REBUTJAT s'abandona al primer intent. És la línia que separa un reintent útil d'un cobrament duplicat.

Solució 3

// src/laboratori/panell-ocupacio.js
// Panell d'ocupacio d'Escena Viva amb parallelisme acotat i mesura del bucle.

const { monitorEventLoopDelay } = require('node:perf_hooks');

const SALES = ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'];
const LATENCIA_MS = 30;

// 1. Consulta simulada d'una sessio.
function consultarOcupacio(sessioId, index) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const aforament = 420;
      const venudes = (index * 37) % aforament;
      resolve({
        sessioId,
        sala: SALES[index % SALES.length],
        percentatge: Math.round((venudes / aforament) * 100),
        recaptacioCentims: venudes * 2500
      });
    }, LATENCIA_MS);
  });
}

// 2. Parallelisme acotat.
async function enLots(elements, midaLot, tasca) {
  const resultats = [];
  for (let i = 0; i < elements.length; i += midaLot) {
    const lot = elements.slice(i, i + midaLot);
    resultats.push(...await Promise.all(lot.map(tasca)));
  }
  return resultats;
}

async function generarPanell(idsSessions, midaLot) {
  return enLots(idsSessions, midaLot, (id, i) => consultarOcupacio(id, i));
}

// 3. Agregacio per sala.
function agregarPerSala(ocupacions) {
  const perSala = new Map();

  for (const o of ocupacions) {
    const a = perSala.get(o.sala) ?? { sala: o.sala, sessions: 0, sumaPercentatges: 0, recaptacioCentims: 0 };
    a.sessions++;
    a.sumaPercentatges += o.percentatge;
    a.recaptacioCentims += o.recaptacioCentims;
    perSala.set(o.sala, a);
  }

  return [...perSala.values()].map((a) => ({
    sala: a.sala,
    sessions: a.sessions,
    ocupacioMitjana: `${Math.round(a.sumaPercentatges / a.sessions)}%`,
    recaptacioEuros: (a.recaptacioCentims / 100).toFixed(2)
  }));
}

async function principal() {
  const ids = Array.from({ length: 300 }, (_, i) => `ses-${String(i + 1).padStart(3, '0')}-1`);

  // 4. Mesura del retard del bucle durant tot el proces.
  const histograma = monitorEventLoopDelay({ resolution: 5 });
  histograma.enable();

  // 5. Comparativa de mides de lot.
  const comparativa = [];
  for (const midaLot of [1, 10, 50, 300]) {
    const inici = Date.now();
    await generarPanell(ids, midaLot);
    comparativa.push({ midaLot, ms: Date.now() - inici });
  }

  histograma.disable();

  const ocupacions = await generarPanell(ids, 50);

  console.log("PANELL D'OCUPACIO");
  console.table(agregarPerSala(ocupacions));

  console.log('');
  console.log('TEMPS SEGONS LA MIDA DE LOT');
  console.table(comparativa);

  const aMs = (n) => (n / 1e6).toFixed(2);
  console.error('');
  console.error(`Retard del bucle: mitjana ${aMs(histograma.mean)} ms, p99 ${aMs(histograma.percentile(99))} ms`);
}

principal().catch((error) => {
  console.error(`Fallada fatal: ${error.message}`);
  process.exitCode = 1;
});
PANELL D'OCUPACIO
┌─────────┬────────────────────┬──────────┬─────────────────┬─────────────────┐
│ (index) │ sala               │ sessions │ ocupacioMitjana │ recaptacioEuros │
├─────────┼────────────────────┼──────────┼─────────────────┼─────────────────┤
│ 0       │ 'Teatro Almendra'  │ 100      │ '49%'           │ '515450.00'     │
│ 1       │ 'Sala Boveda'      │ 100      │ '50%'           │ '523900.00'     │
│ 2       │ 'Auditorio Ribera' │ 100      │ '50%'           │ '521100.00'     │
└─────────┴────────────────────┴──────────┴─────────────────┴─────────────────┘

TEMPS SEGONS LA MIDA DE LOT
┌─────────┬─────────┬──────┐
│ (index) │ midaLot │ ms   │
├─────────┼─────────┼──────┤
│ 0       │ 1       │ 9412 │
│ 1       │ 10      │ 942  │
│ 2       │ 50      │ 192  │
│ 3       │ 300     │ 32   │
└─────────┴─────────┴──────┘

Retard del bucle: mitjana 5.31 ms, p99 11.08 ms

Anàlisi:

  • El lot de 300 és el més ràpid (32 ms contra 9,4 segons) perquè les 300 esperes transcorren simultàniament: el cost total és el d'una sola latència de 30 ms.
  • El retard del bucle es manté entre 5 i 11 ms fins i tot amb 300 operacions en vol. Compara-ho amb l'informe síncron de la lliçó anterior, que disparava el p99 a 414 ms. Aquesta és la demostració que l'asincronia ben feta escala sense bloquejar.
  • I tot i així el lot de 300 no seria l'elecció correcta contra un sistema real: una base de dades amb un grup de 20 connexions no executa 300 consultes simultànies, les encua; una API externa amb límit de 100 peticions per minut et tornaria errors 429; i 300 respostes arribant alhora multipliquen l'ús de memòria. La mida de lot es tria per allò que aguanta el destí, no pas pel que aguanta Node. Un valor entre 10 i 50 és el punt habitual, i s'ajusta mesurant.

Conclusió

Has recuperat el llenguatge. Una promesa és un objecte amb tres estats —pendent, complerta, rebutjada—, que canvia d'estat exactament un cop i el resultat del qual és immutable; aquesta sola propietat elimina per construcció el bug de «el callback s'ha cridat dues vegades». La consumeixes amb .then, .catch i .finally i, com que cadascun torna una promesa nova, els passos s'encadenen plans amb un únic punt de tractament d'errors en lloc de la piràmide de la lliçó anterior.

Has après a crear-les: new Promise(resolve, reject) per embolcallar allò que encara no és una promesa —un temporitzador, un esdeveniment, una passarella de pagament— i, sobretot, util.promisify per convertir d'una tacada les funcions error-first d'Escena Viva que vas escriure ahir. I has vist l'antipatró que cal evitar: no embolcallar mai una promesa dins d'una altra.

Sobre aquesta base, async/await amb les seves tres regles: una funció async sempre torna una promesa, await pausa només la funció que el conté —ho vas demostrar amb un batec que continuava sonant durant l'espera— i try/catch/finally torna a funcionar. El flux de compra complet va passar de 70 línies amb objecte context i dues rutes d'error a 30 línies amb variables locals normals, un sol catch i una compensació de dues línies.

Has interioritzat la distinció que separa el codi lent del ràpid: await en un bucle for sobre tasques independents multiplica el temps pel nombre d'elements, mentre que Promise.all sobre un map el deixa en el cost de la més lenta. I saps triar entre els quatre combinadors: all quan ho necessites tot, allSettled quan les fallades parcials són tolerables i cal reportar-les, race per imposar un temps límit i any per a fonts redundants. També saps que el paral·lelisme s'acota per allò que aguanta el destí, no pas pel que aguanta Node.

Finalment, tens les eines de robustesa que faràs servir sempre: dormir (o node:timers/promises), reintents amb espera exponencial i un predicat esReintentable que evita reintentar un pagament rebutjat, i temps límit amb Promise.race recordant netejar el temporitzador al finally. I saps que un rebuig no gestionat mata el procés des de Node 15, així que la teva funció principal sempre porta el seu .catch.

Queda un cas que ni les promeses ni async/await cobreixen, i no per casualitat: una promesa s'assenta un sol cop, però hi ha resultats que passen moltes vegades al llarg del temps. Una sessió que s'exhaureix, una venda que es registra, un aforament que baixa del 10 %, un socket que rep dades: això no és «un resultat futur», és un flux de successos. Per a això Node té un mecanisme propi que porta al seu ADN des del primer dia. A la lliçó següent, Esdeveniments i EventEmitter, construiràs el GestorDeVendes d'Escena Viva i faràs que el sistema sencer se n'assabenti, sense cap acoblament, en l'instant en què una sessió es queda sense entrades.

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