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
- Què és una promesa i els seus tres estats
- Consumir promeses:
.then,.catchi.finally - L'encadenament pla contra la piràmide
- Crear promeses:
new Promiseiutil.promisify asynciawait: les tres regles- El flux de compra reescrit, costat a costat
- Seqüencial contra concurrent: l'error del
forambawait Promise.all,allSettled,raceiany- Propagació d'errors i rebuigs no gestionats
awaitde nivell superior- Patrons útils:
dormir, reintents i temps límit - Taula resum: callbacks, promeses i
async/await
- 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:
- 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.
- El resultat és immutable. Un cop complerta amb un valor, aquest valor no canvia.
- 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 ... }
- Consumir promeses:
.then, .catch i .finally
.then, .catch i .finallyUna 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.
- 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
.thentornes 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
.thensegü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.
- Crear promeses:
new Promise i util.promisify
new Promise i util.promisify4.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 };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:
resolveirejectno interrompen la funció. Igual que amb els callbacks, posa-hi unreturndesprés.rejectsempre amb un objecteError.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.
async i await: les tres regles
async i await: les tres reglesasync/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 OtonoEncara 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 malamentRegla 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");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');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.
- 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.
- Seqüencial contra concurrent: l'error del
for amb await
for amb awaitasync/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`);
});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`);
});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));
Promise.all, allSettled, race i any
Promise.all, allSettled, race i anyEls 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 |
- 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 profundExactament 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.
await de nivell superior
await de nivell superiorEn 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.
- Patrons útils:
dormir, reintents i temps límit
dormir, reintents i temps límitTres 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.
- Taula resum: callbacks, promeses i
async/await
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/awaitper defecte. Fes servir.thenquan componguis promeses sense necessitat d'esperar-les (Promise.all,.catchen 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:
- Prengui les funcions
cercarEsdeveniment,cercarSessioireservarEntradesde la lliçó anterior i les converteixi ambutil.promisify. - Implementi
carregarCatalegSerie(ids)ambawaiten un buclefor. - Implementi
carregarCatalegParallel(ids)ambPromise.all. - Executi les dues amb els tres esdeveniments, mesuri els temps amb
Date.now()i mostri una taula comparativa ambconsole.tableque inclogui el temps de cadascuna i el factor de millora. - Afegeixi una tercera variant
carregarCatalegResilient(ids)ambPromise.allSettledque funcioni fins i tot si se li passaevt-999, informant de les fallades perstderr.
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:
- Un temps límit de 2 segons al cobrament, fent servir
ambLimitDeTemps. - Reintents del cobrament: fins a 3 intents amb espera exponencial des de 200 ms, només per a errors amb codi
TEMPS_EXHAURIT,ETIMEDOUToPASSARELLA_NO_DISPONIBLE. - Una passarella simulada
cobrarComanda(comanda)que:- Rebutgi amb
PAGAMENT_REBUTJATsi el total supera 20000 cèntims (no reintentable). - Falli amb
PASSARELLA_NO_DISPONIBLEles dues primeres vegades que se la cridi i funcioni a la tercera (reintentable).
- Rebutgi amb
- Compensació correcta amb
try/catch: si alguna cosa falla després de reservar l'aforament, s'allibera. - Registre per
stderrde cada intent, i resultat final perstdout.
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:
- Una funció
consultarOcupacio(sessioId)que torni una promesa amb{ sessioId, sala, percentatge, recaptacioCentims }després d'una latència de 30 ms. generarPanell(idsSessions, midaLot)que faci servir l'ajudantenLotsde l'apartat 7 per consultar totes les sessions amb un màxim demidaLoten vol simultàniament.- Agregació per sala: total de sessions, ocupació mitjana i recaptació en euros.
- Mesura del retard del bucle d'esdeveniments durant la generació, amb
monitorEventLoopDelayde la lliçó anterior, per demostrar que la versió amb promeses no bloqueja encara que processi 300 sessions. - 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è
setTimeoutno 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:
- 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.
PAGAMENT_REBUTJATs'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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
