Ja saps com gira el bucle d'esdeveniments. El que encara no has vist és com se li lliura feina. La resposta, en la Node.js clàssica i encara avui en bona part de la seva API, és el callback: una funció que tu escrius i que Node desa per cridar-la més tard, quan la feina estigui a punt.

Aquesta lliçó ensenya el patró des de zero. No és història antiga: encara que en el dia a dia escriguis async/await, els callbacks continuen estant per sota de tot —els fan servir els streams, els EventEmitter, el servidor HTTP i desenes d'APIs de Node que mai no han rebut una versió amb promeses—, i entendre'n les regles és l'única manera d'entendre què resolen exactament les promeses de la lliçó següent.

Aprendràs la forma canònica del callback en Node (l'error-first callback), escriuràs les teves pròpies funcions asíncrones per a Escena Viva, descobriràs per què try/catch deixa de funcionar tan bon punt hi ha asincronia pel mig, i construiràs amb les teves mans —expressament— la temuda piràmide de la mort. Sentir aquesta incomoditat en carn pròpia és la millor preparació possible per a la lliçó següent.

Contingut

  1. Què és un callback
  2. Síncron contra asíncron: el mateix problema, dues formes
  3. El patró canònic de Node: l'error-first callback
  4. Escriure les teves pròpies funcions asíncrones
  5. La regla d'or: exactament un cop, i sempre asíncronament
  6. Per què try/catch no captura els errors asíncrons
  7. La piràmide de la mort a Escena Viva
  8. Tècniques clàssiques de mitigació
  9. Avantatges i inconvenients dels callbacks
  10. Per què els callbacks continuen important

  1. Què és un callback

Un callback (funció de retorn) és simplement una funció que es passa com a argument a una altra funció perquè aquesta la cridi en algun moment. No és un concepte de Node ni d'asincronia: és una conseqüència del fet que en JavaScript les funcions són valors com els números o les cadenes.

De fet fa temps que fas servir callbacks des del Mòdul 1 sense donar-los aquest nom:

// Callbacks SINCRONS: map, filter i sort criden la teva funcio
// immediatament, tantes vegades com calgui, i tornen el resultat.

const titols = cataleg.map((esdeveniment) => esdeveniment.titol);
const grans = cataleg.filter((esdeveniment) => esdeveniment.sala === 'Auditorio Ribera');
const ordenats = [...cataleg].sort((a, b) => a.titol.localeCompare(b.titol));

Aquests són callbacks síncrons: quan map torna, la teva funció ja s'ha executat totes les vegades necessàries. No hi ha cap bucle d'esdeveniments involucrat.

Els interessants per a nosaltres són els asíncrons:

// Callback ASINCRON: es registra ara i s'executa despres,
// quan el bucle d'esdeveniments arribi a la fase corresponent.

setTimeout(() => {
  console.log("Aixo s'executa despres");
}, 1000);

console.log("Aixo s'executa abans");

La diferència és fonamental i defineix tota la lliçó:

Callback síncron Callback asíncron
Quan s'executa Abans que la funció que el rep torni Després, des del bucle d'esdeveniments
Exemples map, filter, reduce, sort, forEach setTimeout, fs.readFile, servidor.on('request')
Es pot embolcallar en try/catch? Sí No (apartat 6)
Pot tornar un valor útil? Sí, a qui l'ha cridat No: qui l'ha cridat ja va tornar fa estona

  1. Síncron contra asíncron: el mateix problema, dues formes

Plantegem un problema concret d'Escena Viva: cercar un esdeveniment pel seu identificador. Avui les dades són a la memòria, però al Mòdul 3 seran a dades/esdeveniments.json i al Mòdul 7 en una base de dades. És a dir: avui és instantani, demà implicarà esperar.

Versió síncrona, la que sabries escriure sense aquest curs:

// src/laboratori/cercar-sincron.js

const cataleg = [
  { id: 'evt-001', titol: 'Concierto de Otono', sala: 'Teatro Almendra' },
  { id: 'evt-002', titol: 'Noche de Monologos', sala: 'Sala Boveda' },
  { id: 'evt-003', titol: 'Festival de Jazz de Primavera', sala: 'Auditorio Ribera' }
];

function cercarEsdevenimentSincron(id) {
  const esdeveniment = cataleg.find((e) => e.id === id);
  if (!esdeveniment) {
    throw new Error(`Esdeveniment no trobat: ${id}`);
  }
  return esdeveniment;
}

// Us: natural, lineal, amb try/catch que funciona.
try {
  const esdeveniment = cercarEsdevenimentSincron('evt-002');
  console.log(`Trobat: ${esdeveniment.titol}`);
} catch (error) {
  console.error(`Error: ${error.message}`);
}

Tot encaixa: el valor es torna, l'error es llança, i try/catch el recull. És el model mental amb què vas aprendre a programar.

Ara la versió asíncrona, que és la que necessitaràs tan bon punt les dades vinguin de disc o de xarxa:

// src/laboratori/cercar-asincron.js

const cataleg = [
  { id: 'evt-001', titol: 'Concierto de Otono', sala: 'Teatro Almendra' },
  { id: 'evt-002', titol: 'Noche de Monologos', sala: 'Sala Boveda' },
  { id: 'evt-003', titol: 'Festival de Jazz de Primavera', sala: 'Auditorio Ribera' }
];

// El resultat NO es torna: es lliura al callback.
// L'error NO es llanca: es lliura al callback com a primer argument.
function cercarEsdeveniment(id, callback) {
  // setTimeout simula la latencia d'un disc o d'una base de dades.
  setTimeout(() => {
    const esdeveniment = cataleg.find((e) => e.id === id);
    if (!esdeveniment) {
      callback(new Error(`Esdeveniment no trobat: ${id}`));
      return;
    }
    callback(null, esdeveniment);
  }, 50);
}

// Us: el resultat arriba "cap endins" de la funcio, no "cap enfora".
cercarEsdeveniment('evt-002', (error, esdeveniment) => {
  if (error) {
    console.error(`Error: ${error.message}`);
    return;
  }
  console.log(`Trobat: ${esdeveniment.titol}`);
});

console.log("Aquesta linia s'imprimeix ABANS que el resultat");
Aquesta linia s'imprimeix ABANS que el resultat
Trobat: Noche de Monologos

Compara les dues versions amb atenció, perquè el canvi de mentalitat ho és tot:

Aspecte Síncron Asíncron amb callback
Lliurament del resultat return esdeveniment callback(null, esdeveniment)
Lliurament de l'error throw new Error(...) callback(new Error(...))
Flux del codi De dalt a baix Fragmentat: el «després» viu dins del callback
Captura d'errors try/catch Comprovar el primer argument
Mentre espera El procés està bloquejat El procés atén altres coses

Aquesta última fila és la raó de ser de tot. Si cercarEsdeveniment hagués de llegir de disc, la versió síncrona congelaria el servidor d'Escena Viva durant tota la lectura; l'asíncrona deixa el fil lliure per atendre altres compradors.

  1. El patró canònic de Node: l'error-first callback

Node no va deixar la forma del callback a la imaginació de cadascú. Va fixar una convenció que segueix tota la seva biblioteca estàndard i pràcticament tot l'ecosistema:

El callback rep l'error com a primer argument i el resultat com a segon. Si no hi va haver error, el primer argument és null.

function (err, resultat) { /* ... */ }

Se'n diu error-first callback o Node-style callback. Així es veu a l'API real:

const fs = require('node:fs');

fs.readFile('dades/esdeveniments.json', 'utf8', (error, contingut) => {
  if (error) {
    console.error(`No s'ha pogut llegir el cataleg: ${error.message}`);
    return;
  }
  const cataleg = JSON.parse(contingut);
  console.log(`Carregats ${cataleg.length} esdeveniments`);
});

Les regles completes de la convenció:

  1. El callback és sempre l'últim paràmetre de la funció.
  2. El primer argument del callback és sempre l'error, o null si tot va anar bé.
  3. L'error és un objecte Error, no pas una cadena. Un Error porta message, stack i —en Node— sovint un code ('ENOENT', 'EACCES') que permet decidir sense analitzar el text.
  4. Si hi ha error, no hi ha resultat. No es crida amb les dues coses alhora.

I el patró d'ús, que has d'escriure amb pilot automàtic:

funcioAsincrona(parametres, (error, resultat) => {
  if (error) {
    // 1. Tractar l'error
    // 2. RETORNAR. Aquest return es obligatori.
    return;
  }
  // 3. Aqui, i nomes aqui, el resultat es fiable
});

El return després de tractar l'error no és opcional. Sense ell, l'execució continua cap al codi del cas feliç amb resultat valent undefined, i la fallada real queda enterrada sota un TypeError: Cannot read properties of undefined. És, sense exagerar, l'error número u de qui comença amb callbacks.

Per què l'error primer i no el resultat?

Perquè obliga a veure'l. L'error ocupa la primera posició de la signatura, així que apareix a cada callback que escrius encara que no vulguis. Si estigués al final, seria trivial declarar (resultat) => {...} i no assabentar-se mai que alguna cosa pot fallar.

És una decisió de disseny deliberada: fer del camí correcte el camí fàcil.

  1. Escriure les teves pròpies funcions asíncrones

Anem a construir la capa de dades asíncrona d'Escena Viva. Farem servir setTimeout per simular la latència que al Mòdul 3 serà real (disc) i al Mòdul 7 serà de xarxa (base de dades). Això no és cap truc didàctic gratuït: és exactament el que fa un doble de prova al Mòdul 9.

Crea src/laboratori/dades-asincrones.js:

// src/laboratori/dades-asincrones.js
// Capa d'acces a dades d'Escena Viva amb callbacks a l'estil de Node.
// La latencia se simula amb setTimeout; al modul 3 sera E/S real.

const cataleg = [
  {
    id: 'evt-001',
    titol: 'Concierto de Otono',
    sala: 'Teatro Almendra',
    organitzador: 'org-almendra',
    sessions: [
      { id: 'ses-001-1', dataHora: '2026-10-03T20:00:00', aforament: 420, venudes: 180, preuCentims: 2500 },
      { id: 'ses-001-2', dataHora: '2026-10-04T19:00:00', aforament: 420, venudes: 96,  preuCentims: 2200 }
    ]
  },
  {
    id: 'evt-002',
    titol: 'Noche de Monologos',
    sala: 'Sala Boveda',
    organitzador: 'org-boveda',
    sessions: [
      { id: 'ses-002-1', dataHora: '2026-10-10T21:30:00', aforament: 120, venudes: 118, preuCentims: 1800 },
      { id: 'ses-002-2', dataHora: '2026-10-11T21:30:00', aforament: 120, venudes: 45,  preuCentims: 1800 },
      { id: 'ses-002-3', dataHora: '2026-10-17T21:30:00', aforament: 120, venudes: 12,  preuCentims: 1500 }
    ]
  },
  {
    id: 'evt-003',
    titol: 'Festival de Jazz de Primavera',
    sala: 'Auditorio Ribera',
    organitzador: 'org-ribera',
    sessions: [
      { id: 'ses-003-1', dataHora: '2027-04-17T19:00:00', aforament: 900, venudes: 640, preuCentims: 3800 },
      { id: 'ses-003-2', dataHora: '2027-04-18T19:00:00', aforament: 900, venudes: 720, preuCentims: 4200 }
    ]
  }
];

// Latencia simulada, en mil·lisegons.
const LATENCIA_MS = 40;

// --- Cerca d'un esdeveniment pel seu id ---
function cercarEsdeveniment(id, callback) {
  setTimeout(() => {
    const esdeveniment = cataleg.find((e) => e.id === id);

    if (!esdeveniment) {
      // Error amb codi, igual que fa Node: permet decidir sense llegir el text.
      const error = new Error(`Esdeveniment no trobat: ${id}`);
      error.codi = 'ESDEVENIMENT_NO_TROBAT';
      callback(error);
      return;
    }

    callback(null, esdeveniment);
  }, LATENCIA_MS);
}

// --- Cerca d'una sessio pel seu id, a tot el cataleg ---
function cercarSessio(sessioId, callback) {
  setTimeout(() => {
    for (const esdeveniment of cataleg) {
      const sessio = esdeveniment.sessions.find((s) => s.id === sessioId);
      if (sessio) {
        // Tornem tambe l'esdeveniment: qui crida gairebe sempre el necessita.
        callback(null, { esdeveniment, sessio });
        return;
      }
    }

    const error = new Error(`Sessio no trobada: ${sessioId}`);
    error.codi = 'SESSIO_NO_TROBADA';
    callback(error);
  }, LATENCIA_MS);
}

// --- Reserva d'entrades ---
function reservarEntrades(sessioId, quantitat, callback) {
  // Validacio d'arguments ABANS de qualsevol feina.
  if (!Number.isInteger(quantitat) || quantitat < 1) {
    const error = new Error('La quantitat ha de ser un enter positiu');
    error.codi = 'QUANTITAT_INVALIDA';
    // Compte: aqui hi ha un parany. El resolem a l'apartat 5.
    callback(error);
    return;
  }

  cercarSessio(sessioId, (error, resultat) => {
    if (error) {
      callback(error);
      return;
    }

    const { esdeveniment, sessio } = resultat;
    const lliures = sessio.aforament - sessio.venudes;

    if (quantitat > lliures) {
      const fallada = new Error(
        `Aforament insuficient a ${sessioId}: en demanes ${quantitat} i en queden ${lliures}`
      );
      fallada.codi = 'AFORAMENT_INSUFICIENT';
      callback(fallada);
      return;
    }

    // Com que el JavaScript de l'usuari es monofil, aquesta lectura-modificacio-escriptura
    // es atomica: ningu no pot colar-se entre les dues linies.
    sessio.venudes += quantitat;

    callback(null, {
      esdevenimentId: esdeveniment.id,
      sessioId: sessio.id,
      quantitat,
      importCentims: quantitat * sessio.preuCentims,
      lliuresRestants: sessio.aforament - sessio.venudes
    });
  }, LATENCIA_MS);
}

Prova-ho:

// Cas felic
reservarEntrades('ses-002-2', 3, (error, reserva) => {
  if (error) {
    console.error(`No s'ha pogut reservar: ${error.message}`);
    return;
  }
  console.log(
    `Reservades ${reserva.quantitat} entrades de ${reserva.sessioId} ` +
    `per ${(reserva.importCentims / 100).toFixed(2)} EUR. ` +
    `En queden ${reserva.lliuresRestants}.`
  );
});

// Cas d'aforament insuficient: ses-002-1 te 118 de 120 venudes
reservarEntrades('ses-002-1', 5, (error) => {
  if (error) {
    console.error(`[${error.codi}] ${error.message}`);
  }
});
Reservades 3 entrades de ses-002-2 per 54.00 EUR. En queden 72.
[AFORAMENT_INSUFICIENT] Aforament insuficient a ses-002-1: en demanes 5 i en queden 2

Fixa't en dos detalls de disseny que es repetiran en tot el curs:

  • error.codi a més d'error.message. El missatge és per a les persones; el codi és per al programa. Al Mòdul 6 aquest codi es traduirà a un estat HTTP: SESSIO_NO_TROBADA → 404, AFORAMENT_INSUFICIENT → 409.
  • cercarSessio torna { esdeveniment, sessio }. Tornar la informació que qui crida necessitarà de totes maneres evita una segona consulta.

  1. La regla d'or: exactament un cop, i sempre asíncronament

Escriure funcions que reben callbacks és fàcil. Escriure funcions que accepten callbacks correctament té dues regles que no admeten excepció.

Regla 1: crida el callback exactament un cop

Ni cap vegada (qui t'ha cridat espera per sempre) ni dues (el codi de després s'executa duplicat). Aquesta és la causa de l'error clàssic que ja hem esmentat:

// MALAMENT: sense return, el callback es crida DUES vegades quan hi ha error.
function cercarEsdevenimentMalament(id, callback) {
  setTimeout(() => {
    const esdeveniment = cataleg.find((e) => e.id === id);
    if (!esdeveniment) {
      callback(new Error('No trobat'));       // Falta el return
    }
    callback(null, esdeveniment);             // S'executa igualment
  }, 40);
}

cercarEsdevenimentMalament('evt-999', (error, esdeveniment) => {
  if (error) {
    console.error('Error tractat');
    return;
  }
  console.log(esdeveniment.titol);   // TypeError: Cannot read properties of undefined
});
Error tractat
TypeError: Cannot read properties of undefined (reading 'titol')

L'error es va tractar correctament… i tot i així el procés va fallar, perquè el callback es va invocar una segona vegada amb undefined. Un return després de cada crida al callback. Sense excepcions.

Regla 2: crida el callback sempre de manera asíncrona

Aquesta és més subtil i molt més traïdora. Mira un altre cop la validació de reservarEntrades:

function reservarEntrades(sessioId, quantitat, callback) {
  if (!Number.isInteger(quantitat) || quantitat < 1) {
    callback(error);   // <-- Es crida SINCRONAMENT
    return;
  }
  cercarSessio(sessioId, (...) => {
    callback(null, resultat);   // <-- Es crida ASINCRONAMENT
  });
}

Tenim una funció de vegades síncrona i de vegades asíncrona. I això trenca coses:

// src/laboratori/zalgo.js
// Demostra el perill d'una funcio "de vegades sincrona".

let estat = 'sense inicialitzar';

reservarEntrades('ses-002-2', -1, (error) => {
  console.log(`  Dins del callback, estat = "${estat}"`);
});

estat = 'inicialitzat';
console.log(`Despres de la crida, estat = "${estat}"`);
  Dins del callback, estat = "sense inicialitzar"
Despres de la crida, estat = "inicialitzat"

El callback es va executar abans que la línia següent hagués corregut. Amb una quantitat vàlida, en canvi, l'ordre hauria estat el contrari. La mateixa funció produeix dos ordres d'execució diferents segons els seus arguments.

Aquest problema té nom propi a la comunitat de Node —«no alliberis Zalgo», per un article clàssic d'Isaac Schlueter— i produeix errors dels pitjors: intermitents, dependents de les dades, impossibles de reproduir.

El remei és process.nextTick, i aquest és exactament un dels dos casos legítims que vam anunciar a la lliçó del bucle d'esdeveniments:

// BE: el callback SEMPRE s'invoca de manera asincrona.
function reservarEntrades(sessioId, quantitat, callback) {
  if (!Number.isInteger(quantitat) || quantitat < 1) {
    const error = new Error('La quantitat ha de ser un enter positiu');
    error.codi = 'QUANTITAT_INVALIDA';

    // Ajornem la crida per garantir asincronia uniforme.
    process.nextTick(() => callback(error));
    return;
  }

  // ... la resta igual
}

Ara la sortida és sempre la mateixa, amb qualsevol argument:

Despres de la crida, estat = "inicialitzat"
  Dins del callback, estat = "inicialitzat"
Situació Què fer servir
Ruta d'error immediata (validació d'arguments) process.nextTick(() => callback(error))
Resultat que ja tens a la memòria o a la memòria cau process.nextTick(() => callback(null, valor))
Feina llarga que cal trossejar setImmediate

Nota professional. La biblioteca estàndard de Node compleix aquesta regla escrupolosament. fs.readFile amb una ruta inexistent no et torna la crida de manera síncrona: ajorna l'error. Quan escriguis una API asíncrona pròpia, compleix-la tu també. És la diferència entre una biblioteca en què es pot confiar i una que produeix ensurts.

  1. Per què try/catch no captura els errors asíncrons

Aquest és el motiu profund que existeixi la convenció error-first. Anem a demostrar-ho.

// src/laboratori/try-catch-trencat.js
// El try/catch NO captura el que es llanca dins d'un callback asincron.

function operacioQueFalla(callback) {
  setTimeout(() => {
    throw new Error('Fallada dins del callback asincron');
  }, 50);
}

try {
  operacioQueFalla();
  console.log('El try ha acabat sense capturar res');
} catch (error) {
  console.error("Aixo NO s'imprimeix mai:", error.message);
}

console.log("L'script continua...");
El try ha acabat sense capturar res
L'script continua...

/ruta/src/laboratori/try-catch-trencat.js:6
    throw new Error('Fallada dins del callback asincron');
    ^
Error: Fallada dins del callback asincron
    ...
[el proces mor amb codi 1]

Per què? Perquè el bloc try ja havia acabat quan l'error es va llançar. Recorda el bucle d'esdeveniments:

sequenceDiagram
    participant P as Pila de crides
    participant B as Bucle d'esdeveniments
    P->>P: entra al bloc try
    P->>B: setTimeout programa el callback
    P->>P: surt del bloc try (ja ha acabat!)
    P->>P: la pila es buida
    Note over B: passen 50 ms
    B->>P: executa el callback (pila NOVA)
    P--xP: throw sense cap try al voltant
    Note over P: excepció no capturada → el procés mor

try/catch protegeix una regió de la pila de crides, i el callback s'executa en una pila completament nova, creada pel bucle d'esdeveniments molt més tard. No hi ha cap relació entre totes dues.

La conseqüència pràctica, que cal interioritzar:

Dins d'una funció asíncrona, no llancis mai un error cap enfora. Passa'l al callback.

// MALAMENT: ningu no pot capturar aixo.
function reservarMalament(sessioId, quantitat, callback) {
  setTimeout(() => {
    if (quantitat > 10) throw new Error('Maxim 10 entrades per comanda');
    callback(null, { sessioId, quantitat });
  }, 40);
}

// BE: l'error viatja pel canal previst.
function reservarBe(sessioId, quantitat, callback) {
  setTimeout(() => {
    if (quantitat > 10) {
      callback(new Error('Maxim 10 entrades per comanda'));
      return;
    }
    callback(null, { sessioId, quantitat });
  }, 40);
}

I una variant important: sí que pots fer servir try/catch dins del callback, perquè allà sí que ets a la mateixa pila.

fs.readFile('dades/esdeveniments.json', 'utf8', (error, contingut) => {
  if (error) {
    console.error(`Error de lectura: ${error.message}`);
    return;
  }

  // JSON.parse es SINCRON: aqui try/catch si que funciona.
  let cataleg;
  try {
    cataleg = JSON.parse(contingut);
  } catch (fallada) {
    console.error(`El cataleg no es JSON valid: ${fallada.message}`);
    return;
  }

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

Com a xarxa de seguretat d'últim recurs existeix process.on('uncaughtException'), però no és un mecanisme de gestió d'errors: quan salta, l'estat de la teva aplicació és desconegut. Només serveix per registrar la fallada i tancar de manera ordenada. Ho veurem al Mòdul 11.

  1. La piràmide de la mort a Escena Viva

Ara el problema real. El flux de compra complet d'Escena Viva té quatre passos encadenats, i cadascun depèn de l'anterior:

flowchart LR
    A["1. Cercar l'esdeveniment"] --> B["2. Comprovar l'aforament<br/>de la sessio"]
    B --> C["3. Crear la comanda<br/>estat: pendent"]
    C --> D["4. Emetre les entrades<br/>i passar a emes"]

Amb callbacks, «depèn de l'anterior» vol dir «va dins de l'anterior». I el resultat és això:

// src/laboratori/compra-piramide.js
// El flux de compra complet. Funciona, i es un malson de mantenir.

comprarEntrades('asis-001', 'evt-002', 'ses-002-2', 3);

function comprarEntrades(usuariId, esdevenimentId, sessioId, quantitat) {
  cercarEsdeveniment(esdevenimentId, (error, esdeveniment) => {
    if (error) {
      console.error(`[compra] no s'ha trobat l'esdeveniment: ${error.message}`);
      return;
    }

    comprovarAforament(sessioId, quantitat, (error, sessio) => {
      if (error) {
        console.error(`[compra] aforament: ${error.message}`);
        return;
      }

      crearComanda(usuariId, sessio, quantitat, (error, comanda) => {
        if (error) {
          console.error(`[compra] no s'ha pogut crear la comanda: ${error.message}`);
          return;
        }

        cobrarComanda(comanda, (error, comandaPagada) => {
          if (error) {
            // I a mes cal DESFER: alliberar l'aforament reservat.
            alliberarAforament(sessioId, quantitat, (errorAlliberar) => {
              if (errorAlliberar) {
                console.error(`[compra] fallada en alliberar aforament: ${errorAlliberar.message}`);
              }
              console.error(`[compra] cobrament rebutjat: ${error.message}`);
            });
            return;
          }

          emetreEntrades(comandaPagada, (error, entrades) => {
            if (error) {
              console.error(`[compra] no s'han pogut emetre les entrades: ${error.message}`);
              return;
            }

            console.log(`Comanda ${comandaPagada.id} completada`);
            console.log(`Esdeveniment: ${esdeveniment.titol}`);
            console.log(`Import      : ${(comandaPagada.totalCentims / 100).toFixed(2)} EUR`);
            for (const entrada of entrades) {
              console.log(`  ${entrada.codi}  ${entrada.estat}`);
            }
          });
        });
      });
    });
  });
}

A això se'n diu piràmide de la mort (pyramid of doom) o infern de callbacks (callback hell), i no és una qüestió estètica. Els problemes són concrets i mesurables:

Problema Per què fa mal
Indentació creixent Amb sis nivells, el codi útil comença a la columna 30. En pantalles normals no hi cap
Gestió d'errors repetida El mateix if (error) { ...; return; } cinc vegades, i cadascun s'ha d'escriure a mà
Desfer és un infern Mira l'alliberarAforament niat dins de l'error del cobrament: si hi hagués tres coses per desfer, serien tres nivells més
Impossible de llegir en ordre Per saber què passa després del pas 3 cal baixar, no continuar llegint
Difícil de reutilitzar Cap d'aquests passos no es pot extreure sense reescriure-ho tot
Impossible de paral·lelitzar Si els passos 1 i 2 fossin independents, amb aquesta estructura continuarien executant-se en sèrie
Variables atrapades al tancament esdeveniment només està disponible dins del seu nivell; per fer-lo servir a baix cal arrossegar-lo per tota la piràmide

I falta el pitjor: aquest exemple és curt. Un flux real hi afegeix validació d'usuari, comprovació de descomptes, registre d'auditoria i enviament de correu de confirmació. Deu nivells no és cap exageració.

  1. Tècniques clàssiques de mitigació

Abans de les promeses, la comunitat va desenvolupar tècniques per fer això habitable. Continuen sent vàlides i bones pràctiques de disseny per si mateixes.

8.1 Funcions amb nom en lloc d'anònimes

En comptes de niar funcions anònimes, es declara cada pas per separat i es passa per referència:

// src/laboratori/compra-plana.js
// Mateix flux, aplanat amb funcions amb nom.

function comprarEntrades(usuariId, esdevenimentId, sessioId, quantitat) {
  // Context compartit per tots els passos: substitueix els tancaments niats.
  const context = { usuariId, esdevenimentId, sessioId, quantitat };

  cercarEsdeveniment(esdevenimentId, enTrobarEsdeveniment);

  function enTrobarEsdeveniment(error, esdeveniment) {
    if (error) return fallar('esdeveniment', error);
    context.esdeveniment = esdeveniment;
    comprovarAforament(sessioId, quantitat, enComprovarAforament);
  }

  function enComprovarAforament(error, sessio) {
    if (error) return fallar('aforament', error);
    context.sessio = sessio;
    crearComanda(usuariId, sessio, quantitat, enCrearComanda);
  }

  function enCrearComanda(error, comanda) {
    if (error) return fallar('comanda', error);
    context.comanda = comanda;
    cobrarComanda(comanda, enCobrar);
  }

  function enCobrar(error, comandaPagada) {
    if (error) return compensarIFallar(error);
    context.comanda = comandaPagada;
    emetreEntrades(comandaPagada, enEmetre);
  }

  function enEmetre(error, entrades) {
    if (error) return fallar('emissio', error);
    mostrarResum(context, entrades);
  }

  // Un unic punt de tractament d'errors per a tot el flux.
  function fallar(pas, error) {
    console.error(`[compra] fallada al pas "${pas}": ${error.message}`);
  }

  function compensarIFallar(error) {
    alliberarAforament(sessioId, quantitat, (errorAlliberar) => {
      if (errorAlliberar) {
        console.error(`[compra] CRITIC: aforament no alliberat: ${errorAlliberar.message}`);
      }
      fallar('cobrament', error);
    });
  }
}

Guanys immediats:

  • Indentació constant. Cap nivell no passa de dos.
  • El flux es llegeix de dalt a baix, en el mateix ordre en què passa.
  • Un únic punt de tractament d'errors (fallar), no pas cinc còpies.
  • Cada pas és una funció amb nom, la qual cosa vol dir que apareix amb el seu nom a les traces d'error i es pot provar per separat (Mòdul 9).

El preu és l'objecte context: en perdre els tancaments niats cal portar l'estat a mà. És un intercanvi raonable.

8.2 Retorn primerenc

Ja l'hem fet servir, però mereix enunciar-se com a tècnica: tracta l'error i surt, en lloc d'embolcallar el cas feliç en un else.

// Pitjor: el cas felic queda indentat i l'else s'acumula.
cercarEsdeveniment(id, (error, esdeveniment) => {
  if (error) {
    console.error(error.message);
  } else {
    console.log(esdeveniment.titol);
  }
});

// Millor: el cas felic queda al nivell principal.
cercarEsdeveniment(id, (error, esdeveniment) => {
  if (error) return console.error(error.message);
  console.log(esdeveniment.titol);
});

8.3 Modularitzar

Si una funció té més de dos nivells de callback, gairebé sempre està fent més d'una cosa. Al flux de compra, crearComanda + cobrarComanda + compensar és una unitat conceptual (processar el pagament) que pot viure al seu propi fitxer i exposar un sol callback.

A la lliçó Mòduls CommonJS i require() veurem com fer aquesta separació de debò. La idea de fons: la profunditat de la piràmide sol ser un símptoma de mala separació de responsabilitats, no només de la sintaxi dels callbacks.

8.4 Un ajudant per executar passos en sèrie

Quan els passos tenen la mateixa forma, es pot escriure un petit motor:

// src/utils/en-serie.js
// Executa una llista de passos en ordre, passant el context de l'un a l'altre.
// Cada pas te la signatura: (context, seguent) => void
// on seguent es un error-first callback.

function enSerie(passos, context, enAcabar) {
  let index = 0;

  function seguent(error) {
    if (error) {
      enAcabar(error, context);
      return;
    }
    if (index >= passos.length) {
      enAcabar(null, context);
      return;
    }

    const pas = passos[index++];

    // Ajornem per garantir asincronia uniforme (regla d'or 2).
    process.nextTick(() => pas(context, seguent));
  }

  seguent();
}

module.exports = { enSerie };

Ús:

enSerie(
  [
    (ctx, seguent) => cercarEsdeveniment(ctx.esdevenimentId, (e, esdeveniment) => {
      ctx.esdeveniment = esdeveniment;
      seguent(e);
    }),
    (ctx, seguent) => comprovarAforament(ctx.sessioId, ctx.quantitat, (e, sessio) => {
      ctx.sessio = sessio;
      seguent(e);
    }),
    (ctx, seguent) => crearComanda(ctx.usuariId, ctx.sessio, ctx.quantitat, (e, comanda) => {
      ctx.comanda = comanda;
      seguent(e);
    })
  ],
  { usuariId: 'asis-001', esdevenimentId: 'evt-002', sessioId: 'ses-002-2', quantitat: 3 },
  (error, context) => {
    if (error) return console.error(`[compra] ${error.message}`);
    console.log(`Comanda ${context.comanda.id} creada per a ${context.esdeveniment.titol}`);
  }
);

Això és, en essència, el que feia la biblioteca async, omnipresent a l'ecosistema Node entre 2011 i 2016. Si et trobes async.series, async.waterfall o async.parallel en codi heretat, ja saps què són.

I si et sembla que continua sent molta maquinària per a una cosa que hauria de ser senzilla… tens tota la raó. Aquesta és exactament la conclusió a què va arribar la comunitat, i per això van arribar les promeses.

  1. Avantatges i inconvenients dels callbacks

Avantatges Inconvenients
Simplicitat conceptual: és només una funció que es passa com a argument Niament: els fluxos seqüencials creixen en profunditat
Sense cost de rendiment: no hi ha objecte intermedi ni cua de microtasques Gestió d'errors repetitiva: if (error) return a cada nivell
Universals: els suporta qualsevol versió de Node i del navegador try/catch no funciona: el model d'errors del llenguatge no s'hi aplica
Perfectes per a esdeveniments repetits: un callback es pot cridar moltes vegades Fàcils de fer servir malament: cridar dos cops, no cridar, cridar síncronament
Necessaris per a streams i EventEmitter Compondre és difícil: encadenar, paral·lelitzar o cancel·lar requereix ajudants
Menor consum de memòria en operacions molt freqüents Inversió de control: lliures la teva funció a un tercer i confies que la cridi bé

Aquest últim inconvenient mereix una nota. Quan passes un callback a una biblioteca, confies que la cridi un cop, amb els arguments correctes i de manera asíncrona. Si la biblioteca té una fallada, tu en pateixes les conseqüències i depurar-ho és dificilíssim. Les promeses eliminen aquest problema d'arrel, perquè el contracte l'imposa el llenguatge i no pas cada autor.

  1. Per què els callbacks continuen important

Amb async/await disponible des de Node 7.6, per què dedicar una lliçó sencera als callbacks? Quatre raons molt pràctiques:

1. Hi ha APIs de Node que només accepten callbacks. No tot té versió amb promeses. fs.watch, dns.lookup, gran part de crypto, moltes opcions de child_process i —sobretot— tot el que està basat en esdeveniments continuen sent territori de callbacks.

2. EventEmitter és purament de callbacks. Quan escriguis emissor.on('sessio-exhaurida', (dades) => {...}), això és un callback. I no pot ser una promesa, perquè una promesa es resol un sol cop i un esdeveniment s'emet moltes vegades. És el tema de la lliçó Esdeveniments i EventEmitter.

3. Els streams funcionen amb callbacks i esdeveniments. Tot el Mòdul 3 es recolza en stream.on('data', ...), stream.on('end', ...) i en callbacks de finalització.

4. El middleware d'Express és un callback. La signatura (req, res, next) del Mòdul 6 és exactament el patró que acabes d'aprendre, amb next fent el paper de «continua amb el pas següent».

Dit d'una altra manera: les promeses substitueixen els callbacks per a l'asincronia d'un sol resultat, no pas per a tota la resta. Saber quan fer servir cadascun forma part d'escriure Node professionalment, i a això dedicarem una taula de decisió a la lliçó d'EventEmitter.

Errors Comuns i Consells

Error 1: oblidar el return després de tractar l'error. El callback es crida dues vegades i la fallada real queda enterrada sota un TypeError. És l'error número u.

Error 2: llançar excepcions dins d'un callback asíncron. Ningú no les captura; el procés mor. Passa-les sempre pel primer argument del callback.

Error 3: embolcallar una crida asíncrona en try/catch i creure que estàs protegit. El try ja ha acabat quan el callback s'executa.

Error 4: escriure funcions «de vegades síncrones». L'ordre d'execució canvia segons les dades i apareixen errors irreproduïbles. process.nextTick a la ruta ràpida.

Error 5: intentar tornar un valor des d'un callback.

// Aixo NO funciona: cercarEsdeveniment torna undefined molt abans.
function obtenirTitol(id) {
  let titol;
  cercarEsdeveniment(id, (error, esdeveniment) => { titol = esdeveniment.titol; });
  return titol;   // undefined, sempre
}

No hi ha manera de convertir asíncron en síncron. L'única sortida és propagar l'asincronia cap amunt.

Error 6: fer servir callbacks dins de forEach esperant que es respecti l'ordre. forEach no espera res. Els callbacks acaben en ordre aleatori i no hi ha manera de saber quan van acabar tots.

Consell 1: escriu sempre (error, resultat), no pas (err, res). En un gestor d'Express, res significa una cosa molt diferent, i la confusió és real.

Consell 2: posa un codi als teus errors. El missatge és per a les persones; el codi és el que el teu codi fa servir per decidir.

Consell 3: si portes tres nivells de niament, atura't i extreu funcions. No continuïs escrivint cap a la dreta.

Consell 4: no reescriguis codi de callbacks que funciona només per moda. Aprèn a convertir-lo quan toqui —amb util.promisify, a la lliçó següent— però un fs.watch amb el seu callback no necessita cap millora.

Exercicis

Exercici 1: arreglar una funció asíncrona trencada

Aquesta funció té quatre defectes segons el que s'ha après a la lliçó. Troba'ls tots, explica quina conseqüència té cadascun i reescriu-la correctament.

function obtenirOcupacio(sessioId, callback) {
  if (!sessioId) {
    callback("Falta l'identificador de sessio");
  }

  setTimeout(() => {
    const sessio = cercarSessioEnMemoria(sessioId);

    if (!sessio) {
      callback(new Error('No trobada'));
    }

    try {
      const percentatge = Math.round((sessio.venudes / sessio.aforament) * 100);
      callback(null, percentatge);
    } catch (error) {
      throw error;
    }
  }, 30);
}

Exercici 2: l'informe de sessions en risc, en asíncron

A la lliçó El Teu Primer Programa en Node.js vas escriure un informe síncron de sessions amb menys del 20 % venut. Reescriu-lo amb la capa asíncrona d'aquesta lliçó.

Escriu src/laboratori/informe-risc.js amb:

  1. Una funció llistarSessionsEnRisc(llindarPercentatge, callback) que faci servir cercarEsdeveniment per carregar els tres esdeveniments un a un, en sèrie (els ids són evt-001, evt-002 i evt-003) i torni per callback un array d'objectes { esdevenimentId, titol, sessioId, dataHora, percentatge }.
  2. Gestió d'errors correcta: si falla qualsevol esdeveniment, el callback ha de rebre l'error i no s'ha de cridar més vegades.
  3. Sortida per stdout amb console.table i process.exitCode = 1 si hi ha alguna sessió en risc.
  4. El llindar s'ha de poder passar per línia d'ordres: node src/laboratori/informe-risc.js 25.

En acabar, respon: quant triga la teva solució amb una latència de 40 ms per esdeveniment? Quant trigaria si els tres esdeveniments es carreguessin alhora? Per què amb aquesta estructura no ho pots fer?

Exercici 3: aplanar la piràmide de compra

Agafa el flux de comprarEntrades de l'apartat 7 i implementa'l de manera completa i executable, amb aquestes peces simulades (totes amb latència de 30 ms i signatura error-first):

  • crearComanda(usuariId, sessio, quantitat, callback) → torna { id: 'com-001', usuariId, sessioId, quantitat, totalCentims, estat: 'pendent' }.
  • cobrarComanda(comanda, callback) → falla amb codi PAGAMENT_REBUTJAT si totalCentims > 20000; si no, torna la comanda amb estat: 'pagat'.
  • emetreEntrades(comanda, callback) → torna un array de quantitat entrades { codi: 'EV-2026-000001', sessioId, estat: 'valida' } i deixa la comanda en estat: 'emes'.
  • alliberarAforament(sessioId, quantitat, callback) → torna el nombre de lliures després d'alliberar.

Requisits:

  1. Estructura plana, amb funcions amb nom i un objecte context (tècnica 8.1).
  2. Un únic punt de tractament d'errors.
  3. Compensació correcta: si el cobrament falla, s'allibera l'aforament abans d'informar.
  4. Prova els dos camins: ses-002-2 amb 3 entrades (54,00 EUR, ha de funcionar) i ses-003-2 amb 5 entrades (210,00 EUR, s'ha de rebutjar i alliberar l'aforament).

Solucions

Solució 1

Els quatre defectes:

# Defecte Conseqüència
1 Falta el return després de callback("Falta l'identificador...") L'execució continua fins al setTimeout i el callback es crida dues vegades
2 L'error és una cadena, no pas un objecte Error Qui el rep no té stack ni code; a més trenca la convenció
3 Crida síncrona a la ruta de validació Funció «de vegades síncrona»: Zalgo. L'ordre d'execució canvia segons els arguments
4 Falta el return després de callback(new Error('No trobada')) S'executa el try amb sessio valent undefined: TypeError

I un cinquè defecte de bonus: el try/catch que fa throw error és inútil i nociu. Captura l'excepció per tornar-la a llançar dins d'un callback asíncron, on ningú no la pot recollir i on tombarà el procés.

Versió corregida:

// src/laboratori/obtenir-ocupacio.js
// Torna el percentatge d'ocupacio d'una sessio.

function obtenirOcupacio(sessioId, callback) {
  // 1. Validacio d'arguments, ajornada per garantir asincronia uniforme.
  if (!sessioId) {
    const error = new Error("Falta l'identificador de sessio");
    error.codi = 'ARGUMENT_INVALID';
    process.nextTick(() => callback(error));
    return;
  }

  setTimeout(() => {
    const sessio = cercarSessioEnMemoria(sessioId);

    // 2. Error amb return: el callback es crida exactament un cop.
    if (!sessio) {
      const error = new Error(`Sessio no trobada: ${sessioId}`);
      error.codi = 'SESSIO_NO_TROBADA';
      callback(error);
      return;
    }

    // 3. Proteccio real davant de dades incoherents, en lloc d'un try/catch inutil.
    if (!sessio.aforament || sessio.aforament <= 0) {
      const error = new Error(`Aforament invalid a ${sessioId}: ${sessio.aforament}`);
      error.codi = 'DADA_INCOHERENT';
      callback(error);
      return;
    }

    const percentatge = Math.round((sessio.venudes / sessio.aforament) * 100);
    callback(null, percentatge);
  }, 30);
}

Solució 2

// src/laboratori/informe-risc.js
// Sessions amb menys del llindar d'ocupacio, carregant els esdeveniments en serie.

const ESDEVENIMENTS = ['evt-001', 'evt-002', 'evt-003'];
const LLINDAR_PER_DEFECTE = 20;

function llistarSessionsEnRisc(llindarPercentatge, callback) {
  const enRisc = [];
  let index = 0;
  let acabat = false;   // Guarda contra crides multiples al callback.

  function seguentEsdeveniment() {
    if (index >= ESDEVENIMENTS.length) {
      finalitzar(null, enRisc);
      return;
    }

    const esdevenimentId = ESDEVENIMENTS[index++];

    cercarEsdeveniment(esdevenimentId, (error, esdeveniment) => {
      if (error) {
        finalitzar(error);
        return;
      }

      for (const sessio of esdeveniment.sessions) {
        const percentatge = Math.round((sessio.venudes / sessio.aforament) * 100);
        if (percentatge < llindarPercentatge) {
          enRisc.push({
            esdevenimentId: esdeveniment.id,
            titol: esdeveniment.titol,
            sessioId: sessio.id,
            dataHora: sessio.dataHora,
            percentatge
          });
        }
      }

      seguentEsdeveniment();
    });
  }

  // Garanteix que el callback s'invoqui exactament un cop.
  function finalitzar(error, resultat) {
    if (acabat) return;
    acabat = true;
    callback(error, resultat);
  }

  seguentEsdeveniment();
}

// --- Punt d'entrada ---
const llindar = Number(process.argv[2]) || LLINDAR_PER_DEFECTE;
const inici = Date.now();

console.error(`Cercant sessions amb menys del ${llindar}% venut...`);

llistarSessionsEnRisc(llindar, (error, sessions) => {
  if (error) {
    console.error(`No s'ha pogut generar l'informe: ${error.message}`);
    process.exitCode = 2;
    return;
  }

  console.error(`Consultats ${ESDEVENIMENTS.length} esdeveniments en ${Date.now() - inici} ms`);

  if (sessions.length === 0) {
    console.log('Cap sessio en risc.');
    return;
  }

  console.table(sessions);
  process.exitCode = 1;   // Codi diferent de 0 per a un sistema d'avisos.
});
node src/laboratori/informe-risc.js 20
Cercant sessions amb menys del 20% venut...
Consultats 3 esdeveniments en 128 ms
┌─────────┬────────────────┬──────────────────────┬─────────────┬───────────────────────┬─────────────┐
│ (index) │ esdevenimentId │ titol                │ sessioId    │ dataHora              │ percentatge │
├─────────┼────────────────┼──────────────────────┼─────────────┼───────────────────────┼─────────────┤
│ 0       │ 'evt-002'      │ 'Noche de Monologos' │ 'ses-002-3' │ '2026-10-17T21:30:00' │ 10          │
└─────────┴────────────────┴──────────────────────┴─────────────┴───────────────────────┴─────────────┘

Respostes a les preguntes:

  • Amb 40 ms per esdeveniment en sèrie: ~120 ms. Els temps se sumen perquè cada consulta comença quan acaba l'anterior.
  • Si es carreguessin alhora: ~40 ms. El cost seria el de l'esdeveniment més lent, no pas la suma.
  • Per què no ho pots fer amb aquesta estructura: seguentEsdeveniment està construïda sobre la premissa que cada pas crida el següent. Per paral·lelitzar caldrien un comptador de respostes pendents, un array de resultats indexat i una guarda per cridar el callback final només quan el comptador arribi a zero — i tot això sense cridar dues vegades el callback si un falla. És perfectament possible (és el que feia async.parallel), però és maquinària manual i propensa a errors. A la lliçó següent, Promise.all resol exactament això en una línia.

Solució 3

// src/laboratori/compra-plana.js
// Flux de compra complet d'Escena Viva, amb estructura plana i compensacio.

const LATENCIA_MS = 30;
let sequenciaComanda = 0;
let sequenciaEntrada = 0;

// --- Passos simulats ---

function crearComanda(usuariId, sessio, quantitat, callback) {
  setTimeout(() => {
    sequenciaComanda++;
    callback(null, {
      id: `com-${String(sequenciaComanda).padStart(3, '0')}`,
      usuariId,
      sessioId: sessio.id,
      quantitat,
      totalCentims: quantitat * sessio.preuCentims,
      estat: 'pendent'
    });
  }, LATENCIA_MS);
}

function cobrarComanda(comanda, callback) {
  setTimeout(() => {
    if (comanda.totalCentims > 20000) {
      const error = new Error(
        `Pagament rebutjat: ${(comanda.totalCentims / 100).toFixed(2)} EUR supera el limit`
      );
      error.codi = 'PAGAMENT_REBUTJAT';
      callback(error);
      return;
    }
    callback(null, { ...comanda, estat: 'pagat' });
  }, LATENCIA_MS);
}

function emetreEntrades(comanda, callback) {
  setTimeout(() => {
    const any = new Date().getFullYear();
    const entrades = [];
    for (let i = 0; i < comanda.quantitat; i++) {
      sequenciaEntrada++;
      entrades.push({
        codi: `EV-${any}-${String(sequenciaEntrada).padStart(6, '0')}`,
        sessioId: comanda.sessioId,
        comandaId: comanda.id,
        estat: 'valida'
      });
    }
    comanda.estat = 'emes';
    callback(null, entrades);
  }, LATENCIA_MS);
}

function alliberarAforament(sessioId, quantitat, callback) {
  setTimeout(() => {
    cercarSessio(sessioId, (error, resultat) => {
      if (error) {
        callback(error);
        return;
      }
      resultat.sessio.venudes -= quantitat;
      callback(null, resultat.sessio.aforament - resultat.sessio.venudes);
    });
  }, LATENCIA_MS);
}

// --- Flux de compra, pla ---

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) {
    if (error) return fallar('reservar-aforament', error);
    context.aforamentReservat = true;
    context.reserva = reserva;
    cercarSessio(sessioId, enCercarSessio);
  }

  function enCercarSessio(error, resultat) {
    if (error) return compensarIFallar('cercar-sessio', error);
    context.sessio = resultat.sessio;
    crearComanda(usuariId, resultat.sessio, quantitat, enCrearComanda);
  }

  function enCrearComanda(error, comanda) {
    if (error) return compensarIFallar('crear-comanda', error);
    context.comanda = comanda;
    cobrarComanda(comanda, enCobrar);
  }

  function enCobrar(error, comandaPagada) {
    if (error) return compensarIFallar('cobrar', error);
    context.comanda = comandaPagada;
    emetreEntrades(comandaPagada, enEmetre);
  }

  function enEmetre(error, entrades) {
    if (error) return compensarIFallar('emetre', error);
    context.entrades = entrades;
    enAcabar(null, context);
  }

  // Punt unic d'error, sense compensacio.
  function fallar(pas, error) {
    error.pas = pas;
    enAcabar(error, context);
  }

  // Punt unic d'error AMB compensacio de l'aforament ja reservat.
  function compensarIFallar(pas, error) {
    if (!context.aforamentReservat) return fallar(pas, error);

    alliberarAforament(sessioId, quantitat, (errorAlliberar, lliures) => {
      if (errorAlliberar) {
        console.error(`CRITIC: aforament no alliberat a ${sessioId}: ${errorAlliberar.message}`);
      } else {
        console.error(`[compensacio] aforament alliberat a ${sessioId}, en queden ${lliures} lliures`);
      }
      fallar(pas, error);
    });
  }
}

// --- Proves dels dos camins ---

function mostrar(error, context) {
  if (error) {
    console.error(`[${error.codi || 'ERROR'}] fallada a "${error.pas}": ${error.message}`);
    return;
  }
  console.log('');
  console.log(`Comanda ${context.comanda.id} - ${context.comanda.estat}`);
  console.log(`  Esdeveniment: ${context.esdeveniment.titol}`);
  console.log(`  Sessio      : ${context.sessio.id}`);
  console.log(`  Import      : ${(context.comanda.totalCentims / 100).toFixed(2)} EUR`);
  for (const entrada of context.entrades) {
    console.log(`  ${entrada.codi}  ${entrada.estat}`);
  }
}

// Cami felic: 3 x 18,00 = 54,00 EUR
comprarEntrades('asis-001', 'evt-002', 'ses-002-2', 3, mostrar);

// Cami de rebuig: 5 x 42,00 = 210,00 EUR, supera el limit
comprarEntrades('asis-002', 'evt-003', 'ses-003-2', 5, mostrar);

Sortida:

Comanda com-001 - emes
  Esdeveniment: Noche de Monologos
  Sessio      : ses-002-2
  Import      : 54.00 EUR
  EV-2026-000001  valida
  EV-2026-000002  valida
  EV-2026-000003  valida
[compensacio] aforament alliberat a ses-003-2, en queden 180 lliures
[PAGAMENT_REBUTJAT] fallada a "cobrar": Pagament rebutjat: 210.00 EUR supera el limit

L'important d'aquesta solució no és que funcioni, sinó quanta feina estructural ha costat que funcioni:

  • Un objecte context per arrossegar l'estat, perquè els tancaments niats van desaparèixer.
  • Un indicador aforamentReservat per saber si hi ha alguna cosa per compensar.
  • Dos punts de sortida d'error (fallar i compensarIFallar) en lloc d'un.
  • Un callback niat més dins de la mateixa compensació.

Tot això és comptabilitat manual que el llenguatge no t'ajuda a portar. Un try/finally faria la feina de la compensació en tres línies… si try/catch funcionés amb codi asíncron. I aquesta és, justament, la primera cosa que les promeses tornen.

Conclusió

Has après el patró que sosté l'asincronia clàssica de Node. Un callback és una funció que lliures perquè es cridi més tard, i Node li va fixar una forma canònica: l'error-first callback (error, resultat), amb l'error sempre en primer lloc —perquè sigui impossible ignorar-lo— i null quan tot va anar bé. Has escrit la capa de dades asíncrona d'Escena Viva amb aquesta signatura: cercarEsdeveniment, cercarSessio i reservarEntrades, amb errors que porten un codi propi (AFORAMENT_INSUFICIENT, SESSIO_NO_TROBADA) que al Mòdul 6 es traduirà directament a estats HTTP.

Has interioritzat les dues regles que no admeten excepció: cridar el callback exactament un cop —d'aquí el return obligatori després de cada crida— i cridar-lo sempre de manera asíncrona, fins i tot a la ruta de validació ràpida, fent servir process.nextTick per no «alliberar Zalgo» amb una funció que unes vegades és síncrona i altres no. I has vist, demostrat pas a pas sobre la pila de crides, per què try/catch no captura els errors llançats dins d'un callback asíncron: el bloc try ja va acabar quan el bucle d'esdeveniments executa la teva funció en una pila nova.

Després vas construir la piràmide de la mort amb el flux de compra real d'Escena Viva —cercar esdeveniment, comprovar aforament, crear comanda, cobrar, emetre entrades— i vas comprovar que el problema no és estètic: és la gestió d'errors duplicada cinc vegades, la compensació niada dins de l'error del cobrament, les variables atrapades a cada tancament i la impossibilitat de paral·lelitzar allò que és independent. Les tècniques clàssiques de mitigació —funcions amb nom, retorn primerenc, modularitzar i un ajudant enSerie— la van fer habitable, però al preu de portar l'estat a mà en un objecte context i d'escriure maquinària que el llenguatge hauria d'aportar.

I tot i així, els callbacks no són història: els necessites per a EventEmitter, per als streams del Mòdul 3, per al middleware d'Express i per a les moltes APIs de Node que mai no tindran versió amb promeses.

El que falta és una manera que l'asincronia torni a assemblar-se al codi normal: que un resultat es torni, que un error es llanci i es capturi amb try/catch, que els passos independents es llancin alhora amb una sola instrucció i que la compensació s'escrigui en un finally. Això és exactament el que ofereix la lliçó següent, Promeses i async/await, on convertiràs amb util.promisify les funcions que acabes d'escriure i reescriuràs aquesta mateixa piràmide de compra fins a deixar-la plana i llegible de dalt a baix.

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