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

  1. El middleware d'error i la seva signatura de quatre arguments
  2. El gestor per defecte d'Express
  3. Errors síncrons, asíncrons i la gran millora d'Express 5
  4. Una jerarquia d'errors pròpia
  5. ESTAT_PER_CODI troba el seu lloc
  6. Errors operatius enfront d'errors de programació
  7. El gestor central complet
  8. El middleware 404
  9. Errors fora d'Express
  10. Llista de comprovació del mòdul

  1. 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.

  1. 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.

  1. 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.

  1. 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.estat opcional deixa que el domini llanci errors amb només un codi, sense saber res d'HTTP; Error.captureStackTrace elimina el constructor de la traça, que comença on de debò va passar la fallada; i esOperatiu = 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);
}

  1. ESTAT_PER_CODI troba el seu lloc

Al 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';
}

  1. 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.

  1. 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»).

  1. 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.

  1. 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:

  1. 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.registrar pot deixar les entrades descomptades de l'aforament però la comanda sense crear.
  2. 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.
  3. 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'uncaughtException sense 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

  1. Llista de comprovació del mòdul

Abans de donar Escena Viva per acabada, repassa punt per punt:

  • [ ] crearAplicacio() a src/app.js no crida listen i retorna l'aplicació; src/servidor.js crea el servidor http, arrenca i registra l'aturada ordenada.
  • [ ] src/config/index.js és l'únic lloc que llegeix process.env, i valida en arrencar; x-powered-by desactivat i trust proxy d'acord amb el desplegament real.
  • [ ] Routers a src/rutes/ i controladors a src/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() amb limit; express.static amb dotfiles: 'ignore'; helmet actiu amb la CSP ajustada al front-end en comptes de desactivada.
  • [ ] CORS amb llista blanca des de configuració, mai '*' amb credentials; límit de peticions a POST /api/comandes.
  • [ ] Tota entrada validada amb esquema a la vora (i a req.dadesValidades, no reassignant req.query); el domini conserva els seus invariants.
  • [ ] Jerarquia d'errors a src/errors.js i gestor central l'últim; traça a la resposta només fora de producció, i idPeticio a les respostes d'error i als registres.
  • [ ] unhandledRejection i uncaughtException registren i acaben ordenadament; npm audit net 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 TypeError detallat no ajuda el client però a tu et delata; comprova-ho amb NODE_ENV=produccio abans de desplegar. I no continuïs després d'un uncaughtException: el procés queda en estat desconegut, així que registra i acaba.
  • Respondre quan res.headersSent ja és true. Provoca Cannot 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 fa JSON.stringify d'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 :id invà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 traca

En 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

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