Al llarg del mòdul han anat quedant caps solts, tots del mateix tipus. carregarEsdeveniment propaga ESDEVENIMENT_NO_TROBAT amb seguent(error). El domini llança AFORAMENT_INSUFICIENT quan la Sala Bóveda ja no té entrades. express.json() rebutja un cos malformat. El middleware validar de 06-06 crida seguent(new ErrorDeValidacio(...)), una classe que encara no existeix. I el middleware 404 de 06-03 propaga RUTA_NO_TROBADA. Ningú no recull res d'això. En aquesta lliçó construïm la destinació comuna: un gestor central que converteix qualsevol fallada en una resposta coherent, i una política clara sobre què es pot convertir en una resposta amable i què no. És l'última peça del mòdul, i la que dona sentit a la taula ESTAT_PER_CODI del mòdul 4.
Contingut
- El middleware d'error i la seva signatura de quatre arguments
- El gestor per defecte d'Express
- Errors síncrons, asíncrons i la gran millora d'Express 5
- Una jerarquia d'errors pròpia
ESTAT_PER_CODItroba el seu lloc- Errors operatius enfront d'errors de programació
- El gestor central complet
- El middleware 404
- Errors fora d'Express
- Llista de comprovació del mòdul
- El middleware d'error i la seva signatura de quatre arguments
Un middleware d'error és idèntic als altres tret d'una cosa: té quatre paràmetres.
// El primer parametre es l'error que ha arribat per seguent(error) o per un rebuig.
function gestorDErrors(error, peticio, resposta, seguent) {
resposta.status(500).json({ error: { codi: 'ERROR_INTERN', estat: 500 } });
}
aplicacio.use(gestorDErrors); // registrat l'ULTIM
// MALAMENT: nomes tres parametres. Express el tracta com a middleware NORMAL,
// aixi que no rep mai errors i a mes s'executa en peticions sanes.
aplicacio.use((error, peticio, resposta) => resposta.status(500).json({ error: 'fallada' }));Express distingeix els dos tipus inspeccionant fn.length, l'aritat de la funció: si val 4, és gestor d'errors; si no, és middleware normal. Els símptomes de la versió dolenta són desconcertants: els errors continuen mostrant-se amb el format per defecte d'Express, i a les peticions correctes apareix un 500 inexplicable, perquè la teva funció rep (req, res, next) i tracta req com si fos un error. Si no fas servir seguent, declara'l igualment: configura ESLint per permetre arguments no utilitzats al final, o anomena'l _seguent, però no el treguis.
Per què va l'últim
Un gestor d'errors només captura el que s'ha propagat des de middleware i rutes registrats per damunt d'ell. Registrar-lo al mig significa que tot el declarat després escapa al seu control: si escrius aplicacio.use(gestorDErrors) i només després aplicacio.use('/api', crearRutesApi()), els errors de l'API no hi arriben. És el mateix principi de l'ordre de 06-04, aplicat al final de la cadena.
Se'n poden encadenar diversos
Un gestor d'errors pot cridar seguent(error) per passar-lo al següent, igual que a la cadena normal, cosa que serveix per separar responsabilitats: aplicacio.use(registrarError) registra i propaga, aplicacio.use(errorDeValidacio) tracta només un tipus o propaga, i aplicacio.use(gestorFinal) respon sempre. El que no ha de passar mai és que l'últim no respongui.
- El gestor per defecte d'Express
Si no en registres cap, Express té el seu, i fa tres coses assenyades: fa servir error.status o error.statusCode si existeixen (si no, respon 500); fixa les capçaleres que porti error.headers; i envia el cos de l'error, amb una diferència crítica segons l'entorn:
NODE_ENV |
Què envia al cos |
|---|---|
development (o sense definir) |
El missatge i la traça completa en HTML |
production |
Només el text de l'estat: Internal Server Error |
Aquest comportament és correcte i val la pena entendre per què. Una traça de pila revela rutes absolutes del servidor (/home/desplegament/escena-viva/src/...), noms de mòduls interns, versions de biblioteques i de vegades fragments de dades: un mapa de franc per a qui estigui buscant per on entrar. En desenvolupament la vols veure sencera; en producció no ha de sortir mai del servidor. El nostre gestor propi manté exactament aquesta política, però amb el nostre format JSON en comptes d'HTML. Un detall sovint ignorat: Express assumeix NODE_ENV=production per a això, i a Escena Viva fem servir produccio en català, així que cal ser explícits i no dependre de la comparació interna del framework —per això el nostre gestor consulta configuracio.esProduccio.
- Errors síncrons, asíncrons i la gran millora d'Express 5
Els errors síncrons Express sempre els ha capturat: un throw dins d'un gestor síncron va al gestor d'errors, sense més.
Errors asíncrons: el gran canvi
// Express 5: aixo FUNCIONA. El rebuig arriba al gestor d'errors.
aplicacio.get('/api/esdeveniments/:id', async (peticio, resposta) => {
const esdeveniment = await obtenirEsdevenimentPerId(peticio.params.id); // pot rebutjar
resposta.json(esdeveniment.toJSON());
});
// Express 4: l'embolcall que veies a TOTS els projectes, perque sense ell
// la peticio es quedava penjada per sempre.
const asyncHandler = (fn) => (p, r, s) => Promise.resolve(fn(p, r, s)).catch(s);
aplicacio.get('/api/esdeveniments/:id', asyncHandler(gestorAsincron));A Express 4, el framework cridava el gestor, aquest retornava una promesa que ningú no observava, i el rebuig es convertia en un unhandledRejection sense resposta al client: ni error visible, ni 500, ni res, només el client esperant. Si et trobes asyncHandler, express-async-handler, catchAsync o wrapAsync en un projecte, ja saps què són: restes d'Express 4. A Express 5 sobren, i treure'ls elimina una capa de soroll de cada ruta.
| Express 4 | Express 5 | |
|---|---|---|
throw síncron |
Capturat | Capturat |
Promesa rebutjada en gestor, middleware o router.param async |
Petició penjada | Capturada |
Error dins de setTimeout / callback |
No capturat | No capturat (continua sent teu) |
Aquesta última fila és l'excepció que no desapareix. Express només pot observar la promesa que li retornes:
// MALAMENT: ningu no captura aixo. Tomba el proces sencer.
aplicacio.get('/malament', (p, r) => setTimeout(() => { throw new Error('invisible'); }, 100));
// BE: promisificat amb la utilitat dormir.js del modul 2.
aplicacio.get('/be', async () => {
await dormir(100);
throw new Error('aquest si que arriba al gestor d\'errors');
});La regla: si un error pot passar dins d'una devolució de trucada, promisifica aquesta operació. És una raó més per preferir fs.promises i les utilitats dels mòduls 2 i 3.
- Una jerarquia d'errors pròpia
Fins ara creaves errors amb Object.assign(new Error(...), { codi }). Funciona, però no permet distingir tipus amb instanceof, no obliga a res i s'escriu diferent a cada lloc. El formalitzarem.
// src/errors.js — tot el que heretin de l'error base es considera
// OPERATIU: esperable i traduible a una resposta HTTP amable.
class ErrorDAplicacio extends Error {
constructor(missatge, { codi, estat, detalls, causa } = {}) {
super(missatge, { cause: causa });
this.name = new.target.name;
this.codi = codi ?? 'ERROR_INTERN';
this.estat = estat; // opcional: si falta, el dedueix ESTAT_PER_CODI
this.detalls = detalls;
this.esOperatiu = true; // distingeix l'esperable d'un bug
Error.captureStackTrace(this, new.target); // traca neta
}
}
/** 400: les dades d'entrada no tenen la forma esperada. */
class ErrorDeValidacio extends ErrorDAplicacio {
constructor(missatge, detalls = []) {
super(missatge, { codi: 'DADES_INVALIDES', estat: 400, detalls });
}
}
const CODIS_NO_TROBAT = {
esdeveniment: 'ESDEVENIMENT_NO_TROBAT', sessio: 'SESSIO_NO_TROBADA',
comanda: 'COMANDA_NO_TROBADA', ruta: 'RUTA_NO_TROBADA',
};
/** 404: el recurs demanat ('esdeveniment', 'sessio', 'comanda', 'ruta') no existeix. */
class RecursNoTrobat extends ErrorDAplicacio {
constructor(tipus, identificador) {
const codi = CODIS_NO_TROBAT[tipus] ?? 'RECURS_NO_TROBAT';
super(`No s'ha trobat ${tipus} ${identificador}`, { codi, estat: 404 });
this.tipus = tipus;
this.identificador = identificador;
}
}
/** 409: l'operacio no es valida en l'estat actual del recurs. */
class ConflicteDEstat extends ErrorDAplicacio {
constructor(missatge, codi = 'ESTAT_INVALID', detalls) {
super(missatge, { codi, estat: 409, detalls });
}
}
module.exports = { ErrorDAplicacio, ErrorDeValidacio, RecursNoTrobat, ConflicteDEstat };Decisions que mereixen explicació:
this.name = new.target.name: cada subclasse s'identifica amb el seu propi nom als registres, sense repetir-lo a mà; i{ cause }preserva l'error original en envoltar-lo, amb la cadena completa per al registre i sense filtrar-la al client.this.estatopcional deixa que el domini llanci errors amb només un codi, sense saber res d'HTTP;Error.captureStackTraceelimina el constructor de la traça, que comença on de debò va passar la fallada; iesOperatiu = trueés la marca central de l'apartat 6.
Ús al domini, que continua sense conèixer HTTP:
// src/domini/sessio.js — nomes el codi; el 409 el posa el gestor central.
vendre(quantitat) {
if (this.lliures < quantitat) {
throw new ConflicteDEstat(
`La sessio ${this.id} nomes te ${this.lliures} entrades lliures`,
'AFORAMENT_INSUFICIENT',
[{ sessioId: this.id, demanades: quantitat, lliures: this.lliures }]
);
}
return (this.venudes += quantitat);
}
ESTAT_PER_CODI troba el seu lloc
ESTAT_PER_CODI troba el seu llocAl mòdul 4 vas escriure src/servidor/errors-http.js amb estatPerError, cosPerError i la taula ESTAT_PER_CODI, que vivia repartida pels gestors de respostes.js. Ara té un únic client: el gestor central.
// src/servidor/errors-http.js — el mateix del modul 4, ampliat.
const ESTAT_PER_CODI = Object.freeze({
QUANTITAT_INVALIDA: 400, PARAMETRE_INVALID: 400, // peticio mal formada
JSON_INVALID: 400, DADES_INVALIDES: 400,
RUTA_NO_PERMESA: 403, // prohibit
ESDEVENIMENT_NO_TROBAT: 404, SESSIO_NO_TROBADA: 404, // no existeix
COMANDA_NO_TROBADA: 404, RUTA_NO_TROBADA: 404,
METODE_NO_PERMES: 405,
AFORAMENT_INSUFICIENT: 409, ESTAT_INVALID: 409, // conflicte amb l'estat actual
COS_MASSA_GRAN: 413, TIPUS_NO_ACCEPTAT: 415, LIMIT_PER_COMANDA: 422,
MASSA_PETICIONS: 429, SERVEI_EXTERN_CAIGUT: 503,
});
/** Tradueix un error a estat HTTP. Desconegut -> 500. */
function estatPerError(error) {
if (Number.isInteger(error?.estat)) return error.estat; // 1. la nostra jerarquia
const perCodi = ESTAT_PER_CODI[error?.codi]; // 2. codi de domini
if (perCodi) return perCodi;
// 3. Errors de tercers que ja porten estat (express.json, cors, http-errors).
const deTercer = error?.status ?? error?.statusCode;
if (Number.isInteger(deTercer) && deTercer >= 400 && deTercer <= 599) return deTercer;
return 500; // 4. desconegut
}
module.exports = { ESTAT_PER_CODI, estatPerError };La cascada de quatre passos fa que tot encaixi al mateix lloc: la nostra jerarquia, els codis que el domini llança sense saber d'HTTP i els errors de tercers. Per a aquests últims convé a més normalitzar el codi, perquè el client vegi sempre el vocabulari d'Escena Viva:
// src/middleware/errors.js (fragment). PER_TIPUS son errors de body-parser.
const PER_TIPUS = {
'entity.too.large': 'COS_MASSA_GRAN',
'entity.parse.failed': 'JSON_INVALID',
'unsupported.media.type': 'TIPUS_NO_ACCEPTAT',
};
const PER_ESTAT = { 404: 'RUTA_NO_TROBADA', 405: 'METODE_NO_PERMES', 429: 'MASSA_PETICIONS' };
/** Tradueix errors coneguts de tercers al nostre vocabulari. */
function codiPerError(error) {
const perEstat = PER_ESTAT[error?.status ?? error?.statusCode];
return error?.codi ?? PER_TIPUS[error?.type] ?? perEstat ?? 'ERROR_INTERN';
}
- Errors operatius enfront d'errors de programació
Aquesta distinció és la que decideix què s'explica al client i què no:
| Operatiu | De programació (bug) | |
|---|---|---|
| Què és | Situació esperada del món real | Defecte al teu codi |
| Exemples | Aforament insuficient, esdeveniment inexistent, JSON malformat, servei extern caigut | undefined is not a function, TypeError, variable mal escrita |
| El vas preveure? Es resol desplegant? | Sí, és al disseny; no es resol | No; sí |
| Resposta al client i registre | Missatge específic i útil; una línia informativa | 500 genèric, sense detalls; traça completa, cos, context i alerta |
| Pot continuar el procés? | Sí, amb normalitat | Depèn: pot estar en estat inconsistent |
Com es distingeixen al codi:
function esOperatiu(error) {
// 1. La nostra jerarquia ho marca explicitament.
if (error instanceof ErrorDAplicacio) return true;
// 2. Codi de domini conegut a la taula.
if (error?.codi && ESTAT_PER_CODI[error.codi]) return true;
// 3. Errors de tercers amb estat 4xx: peticio dolenta, no bug nostre.
const estat = error?.status ?? error?.statusCode;
if (Number.isInteger(estat) && estat >= 400 && estat < 500) return true;
return false; // tota la resta es sospitosa de bug
}Per què el bug retorna un 500 mut. Si un TypeError es filtra al client amb el seu missatge, publiques el nom de les teves variables internes, la línia del fitxer i, amb la traça, mig arbre de directoris. A més, aquest missatge no li serveix de res a qui crida l'API: no pot corregir la seva petició perquè el problema no hi és. Un 500 amb l'identificador de la petició és més útil per a tothom: el client sap que la fallada és teva i té una referència amb què reclamar, i tu tens la traça completa als teus registres.
- El gestor central complet
// src/middleware/errors.js — codiPerError i esOperatiu: apartats 5 i 6.
// Requereix configuracio (config/index.js), ErrorDAplicacio (errors.js) i
// ESTAT_PER_CODI + estatPerError (servidor/errors-http.js).
const MISSATGE_GENERIC =
"S'ha produit un error intern. Indica l'identificador de peticio al suport.";
// Gestor central; es registra l'ULTIM a crearAplicacio().
function gestorDErrors(error, peticio, resposta, seguent) {
// 1. Amb la resposta ja comencada (un stream que ha fallat a mitges) no es poden
// canviar capcaleres: es delega a Express, que tanca la connexio.
if (resposta.headersSent) {
seguent(error);
return;
}
const estat = estatPerError(error);
const operatiu = esOperatiu(error);
const idPeticio = peticio.idPeticio ?? '-';
const on = `${idPeticio} ${peticio.method} ${peticio.originalUrl}`;
// 2. REGISTRE per stderr, complet. Un bug es registra sencer: traca i causa.
if (operatiu) {
console.error(`[error] ${on} ${estat} ${codiPerError(error)} ${error.message}`);
} else {
console.error(`[BUG] ${on} ${estat} ${error?.name}`);
console.error(error?.stack ?? error);
if (error?.cause) console.error('Causa:', error.cause);
}
// 3. RESPOSTA. Nomes l'operatiu s'explica; el bug queda mut.
const cos = { error: operatiu
? { codi: codiPerError(error), missatge: error.message, estat, idPeticio }
: { codi: 'ERROR_INTERN', missatge: MISSATGE_GENERIC, estat: 500, idPeticio } };
// Detalls: nomes els de la nostra jerarquia (validacio, conflictes).
if (operatiu && error.detalls?.length > 0) cos.error.detalls = error.detalls;
// 4. La traca NOMES fora de produccio, igual que fa Express per defecte.
if (!configuracio.esProduccio && error?.stack) {
cos.error.traca = error.stack.split('\n').map((linia) => linia.trim());
}
// 5. Capcaleres que alguns errors necessiten.
if (estat === 405 && error?.metodesPermesos) {
resposta.set('Allow', error.metodesPermesos.join(', '));
}
if (estat === 503) resposta.set('Retry-After', '30');
resposta.status(cos.error.estat).json(cos);
}
module.exports = { gestorDErrors, codiPerError, esOperatiu };Dues respostes reals, una d'operativa i una altra de bug:
{ "error": { "codi": "AFORAMENT_INSUFICIENT",
"missatge": "La sessio ses-001-1 nomes te 3 entrades lliures", "estat": 409,
"idPeticio": "9f2a1c48-3c7e-4a1b-9c62-1d0f8b4a77e1",
"detalls": [{ "sessioId": "ses-001-1", "demanades": 6, "lliures": 3 }] } }
{ "error": { "codi": "ERROR_INTERN", "estat": 500,
"missatge": "S'ha produit un error intern. Indica l'identificador de peticio al suport.",
"idPeticio": "3b71e0a2-55d4-4a7c-8e19-6f0c2b9d1a44" } }La segona no diu absolutament res de la fallada, però als registres del servidor hi ha la traça completa associada a aquest mateix idPeticio que el client té al davant. Aquesta correlació —l'identificador de 06-04 apareixent a la resposta i al registre— és el que converteix un informe d'incidència inútil («em va donar error») en un d'accionable («em va donar error, referència 3b71e0a2»).
- El middleware 404
// src/middleware/no-trobat.js — es registra despres de TOTES les rutes i
// abans del gestor d'errors. No respon: propaga.
function rutaNoTrobada(peticio, resposta, seguent) {
seguent(new RecursNoTrobat('ruta', `${peticio.method} ${peticio.originalUrl}`));
}
module.exports = { rutaNoTrobada };Per què va abans del gestor d'errors i després de les rutes: és un middleware normal, no d'error, i Express hi arriba quan cap ruta anterior no ha respost. Si el posessis abans de les rutes, respondria 404 a tot; si el posessis després del gestor d'errors, no s'executaria mai. I propaga en comptes de respondre perquè el 404 passi pel mateix punt que la resta d'errors i surti amb codi, estat, idPeticio i el mateix format: curl -s localhost:3000/api/no-existeix retorna {"error":{"codi":"RUTA_NO_TROBADA","missatge":"No s'ha trobat ruta GET /api/no-existeix",...}}. Un client que sap interpretar els teus errors no hauria de necessitar cap cas especial per al 404.
- Errors fora d'Express
Express només veu el que passa dins d'una petició; hi ha dos successos del procés que se li escapen:
// src/servidor.js — promesa rebutjada sense cap .catch() ni try/catch: la
// convertim en excepcio perque passi pel gestor de sota.
process.on('unhandledRejection', (rao) => {
console.error('[FATAL] unhandledRejection:', rao);
throw rao instanceof Error ? rao : new Error(String(rao));
});
// Excepcio que no ha capturat ningu: el proces esta en estat desconegut.
process.on('uncaughtException', (error) => {
console.error('[FATAL] uncaughtException:', error?.stack ?? error);
tancarIAcabar(1); // politica: registrar i acabar ORDENADAMENT
});
// tancarIAcabar deixa d'acceptar peticions noves (servidorActiu.close +
// closeIdleConnections), espera les que hi ha en curs i surt; amb un
// setTimeout(...).unref() de 5 s com a xarxa de seguretat si alguna cosa s'encalla.Per què NO es continua com si res
La temptació és evident: capturar l'error, registrar-lo i deixar el servidor dret per no perdre el servei. És un error greu, i aquestes són les raons:
- L'estat del procés és desconegut. L'excepció s'ha propagat per una pila que no l'esperava: hi ha funcions a mitges, amb variables actualitzades parcialment. A Escena Viva, una excepció enmig de
GestorDeVendes.registrarpot deixar les entrades descomptades de l'aforament però la comanda sense crear. - Es perden recursos. Cada excepció no capturada pot deixar sense tancar un descriptor de fitxer, un socket o un temporitzador: el procés «sobreviu» degradant-se fins a exhaurir la memòria.
- Amaga el bug. Un servidor que continua funcionant malament no genera urgència, i la fallada s'acumula durant setmanes. A més, el comportament per defecte d'
uncaughtExceptionsense gestor és acabar, i així ha de ser.
La política correcta, i la que apliquem, és: registrar l'error sencer → deixar d'acceptar peticions noves → donar uns segons a les peticions en curs → acabar el procés → que el supervisor el reiniciï. Qui reinicia és assumpte de l'entorn, i al mòdul 11 ho veurem amb noms propis: PM2 en mode cluster, restart: always a Docker o el reinici automàtic d'un contenidor gestionat. L'aplicació no es reinicia a si mateixa: es mor amb dignitat i deixa que el supervisor faci la seva feina.
| Succés | Què fer | Què NO fer |
|---|---|---|
seguent(error) operatiu |
Respondre amb 4xx | Acabar el procés |
| Bug dins d'una petició | 500 mut, traça registrada, el procés continua | Filtrar la traça al client |
unhandledRejection i uncaughtException |
Registrar i acabar ordenadament | Ignorar-ho o continuar servint peticions |
SIGTERM |
Aturada ordenada, sortida 0 | Morir de cop |
- Llista de comprovació del mòdul
Abans de donar Escena Viva per acabada, repassa punt per punt:
- [ ]
crearAplicacio()asrc/app.jsno cridalisteni retorna l'aplicació;src/servidor.jscrea el servidorhttp, arrenca i registra l'aturada ordenada. - [ ]
src/config/index.jsés l'únic lloc que llegeixprocess.env, i valida en arrencar;x-powered-bydesactivat itrust proxyd'acord amb el desplegament real. - [ ] Routers a
src/rutes/i controladors asrc/controladors/, tots dos sense lògica de negoci; rutes ordenades d'específica a genèrica, amb el comodí l'últim. - [ ] Tots els camins de cada middleware responen o criden
seguent, i l'ordre és el canònic de 06-05: id, seguretat, CORS, registre, compressió, límits, cos, estàtics, rutes, 404, errors. - [ ]
express.json()amblimit;express.staticambdotfiles: 'ignore'; helmet actiu amb la CSP ajustada al front-end en comptes de desactivada. - [ ] CORS amb llista blanca des de configuració, mai
'*'ambcredentials; límit de peticions aPOST /api/comandes. - [ ] Tota entrada validada amb esquema a la vora (i a
req.dadesValidades, no reassignantreq.query); el domini conserva els seus invariants. - [ ] Jerarquia d'errors a
src/errors.jsi gestor central l'últim; traça a la resposta només fora de producció, iidPeticioa les respostes d'error i als registres. - [ ]
unhandledRejectioniuncaughtExceptionregistren i acaben ordenadament;npm auditnet i dependències revisades amb el criteri del mòdul 5.
Errors Comuns i Consells
- Oblidar el quart paràmetre. Sense
seguent, Express tracta el teu gestor com a middleware normal: no rep mai errors i trenca les peticions sanes. - Registrar-lo abans que les rutes. Només captura el declarat per damunt.
- Filtrar la traça en producció o tractar els bugs com a errors operatius. Publiques rutes del sistema i versions, i un
TypeErrordetallat no ajuda el client però a tu et delata; comprova-ho ambNODE_ENV=produccioabans de desplegar. I no continuïs després d'ununcaughtException: el procés queda en estat desconegut, així que registra i acaba. - Respondre quan
res.headersSentja éstrue. ProvocaCannot set headers after they are sent; comprova l'indicador i delega en Express. - Perdre la causa en envoltar errors. Fes servir
new Error(missatge, { cause: original }); sense això la traça comença on vas envoltar, no on va fallar. I mantén trivial el gestor mateix: si faJSON.stringifyd'alguna cosa amb referències circulars, fallarà al pitjor lloc possible. - Consell: prova els teus errors. Al mòdul 9, amb supertest, verifica que un aforament insuficient retorna 409 amb
AFORAMENT_INSUFICIENT, que un:idinvàlid retorna 400 i que un bug forçat retorna un 500 sense traça en producció.
Exercicis
Exercici 1: la jerarquia en acció
Implementa src/errors.js complet i fes que el domini el faci servir. Comprova amb curl quatre casos, tots amb idPeticio: GET /api/esdeveniments/evt-999 → 404 ESDEVENIMENT_NO_TROBAT; POST /api/comandes amb quantitat: 99 → 400 DADES_INVALIDES amb detalls; POST /api/comandes demanant més entrades de les lliures → 409 AFORAMENT_INSUFICIENT; i GET /api/no-existeix → 404 RUTA_NO_TROBADA.
Exercici 2: el bug que no es filtra
Afegeix una ruta /api/depuracio/fallada que provoqui un TypeError real (llegir una propietat d'undefined). Comprova que amb NODE_ENV=desenvolupament la resposta inclou traca, que amb NODE_ENV=produccio retorna el 500 mut, i que en tots dos casos stderr mostra la traça completa amb el prefix [BUG] i l'idPeticio.
Exercici 3: l'embolcall d'Express 4 i per què sobra
Crea dues rutes async que rebutgin, una envoltada en asyncHandler i l'altra sense envoltar, i comprova a Express 5 que es comporten igual. Després escriu un gestor que llanci dins d'un setTimeout i explica per què aquest sí que tomba el procés, i com ho arreglaries.
Solucions
Solució 1
curl -s localhost:3000/api/esdeveniments/evt-999 # 404 ESDEVENIMENT_NO_TROBAT
curl -s localhost:3000/api/no-existeix # 404 RUTA_NO_TROBADA
# 400 de validacio amb detalls, i 409 de conflicte d'estat:
curl -s -X POST localhost:3000/api/comandes -H 'Content-Type: application/json' \
-d '{"sessioId":"ses-001-1","quantitat":99,"correu":"[email protected]"}'
# {"error":{"codi":"DADES_INVALIDES",...,"detalls":[{"camp":"quantitat","tipus":"too_big"}]}}
curl -s -X POST localhost:3000/api/comandes -H 'Content-Type: application/json' \
-d '{"sessioId":"ses-003-2","quantitat":6,"correu":"[email protected]"}'
# {"error":{"codi":"AFORAMENT_INSUFICIENT","missatge":"...nomes 3 entrades lliures","estat":409}}Fixa't en la diferència entre els dos últims: quantitat: 99 és un problema de forma (400, el detecta l'esquema sense mirar l'estat del sistema); demanar 6 entrades quan en queden 3 és un problema d'estat (409, només el pot detectar el domini en aquell instant). Són els dos nivells de 06-06 funcionant.
Solució 2
// src/rutes/index.js — la ruta es registra nomes fora de produccio: una
// porta per provocar errors no hauria d'existir al desplegament real.
if (!configuracio.esProduccio) {
const sessio = undefined;
api.get('/depuracio/fallada', (p, r) => r.json({ aforament: sessio.aforament })); // TypeError
}NODE_ENV=desenvolupament node src/servidor.js && curl -s localhost:3000/api/depuracio/fallada
# {"error":{"codi":"ERROR_INTERN",...,"traca":["TypeError: Cannot read properties...",...]}}
NODE_ENV=produccio node src/servidor.js && curl -s localhost:3000/api/depuracio/fallada
# {"error":{"codi":"ERROR_INTERN","estat":500,"idPeticio":"..."}} sense tracaEn tots dos casos, stderr mostra la mateixa traça completa precedida de [BUG] 3b71e0a2-... GET /api/depuracio/fallada 500 TypeError.
Solució 3
// comparacio.js — el gestor d'errors final respon 500 amb error.message.
const asyncHandler = (fn) => (p, r, s) => Promise.resolve(fn(p, r, s)).catch(s);
// A envoltada a l'estil Express 4; B sense envoltar: a Express 5 son identiques.
aplicacio.get('/a', asyncHandler(async () => { throw new Error('fallada a A'); }));
aplicacio.get('/b', async () => { throw new Error('fallada a B'); });
// C: dins d'un setTimeout, Express no ho pot veure. D: la versio correcta.
aplicacio.get('/c', (p, r) => setTimeout(() => { throw new Error('fallada a C'); }, 50));
aplicacio.get('/d', async () => { await dormir(50); throw new Error('fallada a D'); });Amb curl, /a, /b i /d retornen un 500 amb el seu missatge —les dues primeres, idèntiques—, mentre que /c mata el procés amb un uncaughtException, perquè el seu throw passa en un tic posterior del bucle d'esdeveniments (mòdul 2), en una pila on ja no hi ha res d'Express, i la promesa que el gestor va retornar —cap, en aquest cas— no pot observar aquesta fallada. L'arranjament és /d: promisificar l'espera perquè el throw passi dins de la cadena async que Express sí que observa. Conclusió pràctica: asyncHandler és innecessari a Express 5, però promisificar continua sent obligatori.
Conclusió
Amb aquesta lliçó es tanca el mòdul 6, i Escena Viva ja és una API Express completa. Els errors tenen per fi una destinació única. Saps que el gestor d'error s'identifica pels seus quatre paràmetres i que oblidar l'últim el converteix en un middleware normal amb símptomes desconcertants; que va registrat l'últim perquè només veu el declarat per damunt; i que el gestor per defecte d'Express filtra la traça en producció i la mostra en desenvolupament, política que el teu gestor propi reprodueix amb el teu format JSON. Has vist la gran millora d'Express 5 —un gestor async que rebutja arriba sol al gestor d'errors, i asyncHandler passa a ser una relíquia d'Express 4— amb la seva única excepció viva: el que passa dins d'una devolució de trucada continua sent cosa teva, i per això es promisifica.
Has construït una jerarquia pròpia a src/errors.js amb ErrorDAplicacio i les seves subclasses ErrorDeValidacio, RecursNoTrobat i ConflicteDEstat, i la taula ESTAT_PER_CODI que vas escriure al mòdul 4 ha trobat per fi el seu lloc definitiu: un únic punt on els codis del domini, els de la teva jerarquia i els dels paquets de tercers es tradueixen a estats HTTP. Distingeixes els errors operatius —esperables, explicats al client amb detall— dels errors de programació —registrats sencers, retornats com un 500 mut amb només l'identificador de petició com a referència—. I fora d'Express, unhandledRejection i uncaughtException registren i acaben el procés ordenadament, sense fingir que no ha passat res, deixant el reinici a un supervisor que arribarà al mòdul 11.
Mira ara el projecte sencer. crearAplicacio() es prova sense arrencar res. La configuració es llegeix i es valida en un sol lloc. Els routers de /api/esdeveniments, /api/sessions i /api/comandes viuen en fitxers de trenta línies amb els seus controladors al costat. Els teus middleware propis mesuren, identifiquen i controlen la memòria cau; els de tercers afegeixen capçaleres de seguretat, CORS, registre, compressió i límits d'ús. Els esquemes de zod descriuen exactament què accepta cada endpoint. I qualsevol fallada, vingui d'on vingui, surt amb la mateixa forma i amb un identificador que l'enllaça amb els registres del servidor. Això és una API de producció. Però hi ha una cosa que no ha canviat des del mòdul 3, i ja comença a incomodar: les dades continuen vivint a dades/esdeveniments.json. Cada venda reescriu un fitxer sencer. Dos compradors que premen «comprar» alhora per a les últimes entrades de la Sala Bóveda llegeixen tots dos el mateix aforament, tots dos veuen que hi ha lloc i tots dos escriuen. El fitxer acaba amb les vendes del segon i les del primer desapareixen, o pitjor: es venen més entrades de les que caben a la sala. Cap validació d'esquema no evita això, cap middleware no ho detecta i cap gestor d'errors no ho pot arreglar, perquè no és un error: és una cursa entre dues escriptures.
Al mòdul 7 les dades deixen de viure en un JSON. Començarem per entendre què és una base de dades i què aporta enfront d'un fitxer, modelarem Escena Viva amb MongoDB i Mongoose, escriurem el CRUD complet, explorarem relacions i consultes avançades, veurem el món SQL amb Sequelize i acabarem amb migracions, llavors i —el que estaves esperant— transaccions, que són exactament la resposta al problema de l'aforament i la sobrevenda que acabem de descriure. El JSON ens ha servit durant set mòduls; ha arribat el moment de jubilar-lo.
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
