El mòdul dades/backlog.js amb què vas tancar la lliçó anterior té una mentida a dins: retorna les sis tasques instantàniament, perquè estan escrites a mà al mateix fitxer. A l'aplicació real aquelles dades viuran en un servidor, i demanar-les trigarà entre cinquanta mil·lisegons i uns quants segons, segons la xarxa. La pregunta que obre aquest bloc del curs és què fa el programa mentrestant. I la resposta òbvia —«esperar»— resulta ser catastròfica en un llenguatge que executa una sola cosa alhora: mentre espera, no pot fer absolutament res més, així que la pàgina es congela. En aquesta lliçó entendràs per què JavaScript necessita l'asincronia, aprendràs les eines més antigues per gestionar-la —setTimeout, setInterval i el patró callback—, simularàs la càrrega del backlog amb latència, i arribaràs pel teu propi peu al famós callback hell, la piràmide de codi que va motivar la invenció de les promeses.
Contingut
- Un sol fil: què significa i per què importa
- Síncron davant d'asíncron, en una línia temporal
setTimeout: programar per a més tardsetInterval,clearTimeouticlearInterval- Per què
setTimeout(f, 0)no és immediat - El patró callback
- La convenció error-first
- Simular la càrrega del backlog
- Encadenar tres operacions: la piràmide
- Els quatre problemes dels callbacks
try/catchno captura errors asíncrons- Mitigacions parcials i per què no en tenen prou
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Un sol fil: què significa i per què importa
JavaScript és un llenguatge d'un sol fil (single-threaded): existeix una única pila de crides —la que vas estudiar a 03-05— i s'hi executa una sola funció a cada instant. No hi ha dos trossos del teu codi corrent alhora.
Això té un avantatge enorme, que s'aprecia millor si has patit altres llenguatges: mai no t'has de preocupar que un altre fil canviï una variable a mitja funció. L'array de tasques no es pot modificar mentre el teu reduce el recorre. Tota la classe d'errors anomenada «condicions de carrera sobre memòria compartida» senzillament no existeix.
I té un inconvenient igual de gran: si una funció triga, tota la resta espera. Al navegador, aquesta «resta» inclou repintar la pantalla, respondre als clics i animar qualsevol cosa.
'use strict';
/** Bloqueja el fil durant els mil·lisegons indicats. NO ho facis en producció. */
function esperarBloquejant(ms) {
const fi = Date.now() + ms;
while (Date.now() < fi) {
// girar en va, cremant el processador
}
}
console.log('Carregant el backlog…');
esperarBloquejant(3000); // 3 segons de paràlisi total
console.log('Backlog carregat');Durant aquells tres segons la pestanya és morta: els botons no responen, el text no es pot seleccionar i, si el navegador decideix que ja n'hi ha prou, apareix l'avís de «la pàgina no respon». Un usuari abandona molt abans.
flowchart TD
A["Pila de crides<br/>(una de sola)"] --> B["esperarBloquejant(3000)<br/>ocupa la pila 3 s"]
B --> C["⛔ Res més no es pot executar:<br/>ni repintat, ni clics, ni temporitzadors"]
La solució no és afegir fils, sinó no esperar: demanar la dada, dir què fer quan arribi, i tornar el control immediatament perquè el programa continuï viu. Això és la programació asíncrona.
- Síncron davant d'asíncron, en una línia temporal
Compara els dos estils sobre el mateix problema: carregar el backlog i pintar el resum.
// ── SÍNCRON (hipotètic): la funció retorna el resultat ────────────
const backlog = carregarBacklogBloquejant(); // ⏳ el programa s'atura aquí
console.log(backlog.length); // 6
console.log('Llest');// ── ASÍNCRON: la funció no retorna res; avisa quan acaba ──────────
carregarBacklog((tasques) => { // ← què fer QUAN arribi
console.log(tasques.length); // 6
});
console.log('Llest'); // ← s'executa ABANS que el 6Aquell canvi d'ordre és el primer que desconcerta. La sortida del segon bloc és:
Perquè carregarBacklog no espera: registra la funció que li passes, torna el control immediatament i el programa continua. Quan les dades estan disponibles —deu, cent o mil mil·lisegons després—, s'executa la funció registrada.
flowchart TD
subgraph S["Síncron · 3,1 s de bloqueig"]
S1["carregarBacklog<br/>0 → 3000 ms<br/>⛔ fil bloquejat"] --> S2["console.log(6)<br/>3000 ms"] --> S3["console.log('Llest')<br/>3001 ms"]
end
subgraph A["Asíncron · 1 ms de fil ocupat"]
A1["carregarBacklog(cb)<br/>0 ms · registra i torna"] --> A2["console.log('Llest')<br/>1 ms"]
A2 --> A3["… el fil està LLIURE<br/>1 → 3000 ms"]
A3 --> A4["cb(tasques) · console.log(6)<br/>3000 ms"]
end
A la versió asíncrona les dades arriben en el mateix instant —la xarxa triga el que triga—, però durant aquells tres segons el fil està lliure: la interfície respon, les animacions corren, altres dades es poden demanar en paral·lel.
Un matís important que sovint s'explica malament: l'asincronia no fa res més ràpid. El que fa és no malgastar el fil mentre s'espera alguna cosa que no en depèn. L'espera la fa un altre: el sistema de xarxa del navegador, el temporitzador del sistema operatiu, el disc. El teu codi només s'apunta al fet que l'avisin.
setTimeout: programar per a més tard
setTimeout: programar per a més tardLa forma més simple de codi asíncron és setTimeout, que registra una funció per executar-se després d'un temps mínim.
'use strict';
console.log('1 · abans');
setTimeout(() => {
console.log('3 · dins del timeout');
}, 1000);
console.log('2 · després');
// Sortida:
// 1 · abans
// 2 · després
// 3 · dins del timeout ← un segon més tardLa seva signatura completa:
funcio: què executar. És un callback: una funció que escrius tu però que crida un altre.millisegons: el retard mínim. Si s'omet, val 0.arg1, arg2…: arguments que es passaran al callback.- Retorna un identificador, que serveix per cancel·lar-lo.
setTimeout((persona, hores) => {
console.log(`Recordatori: ${persona} té ${hores} h obertes`);
}, 500, 'Iván', 25);
// Recordatori: Iván té 25 h obertesUna alternativa que veuràs molt i que convé evitar: passar els arguments amb una fletxa que els captura.
setTimeout(() => avisar('Iván', 25), 500); // ✓ perfectament legítim i més llegible
setTimeout(avisar('Iván', 25), 500); // ✗ ERROR: crida avisar JA i passa el seu retorn
setTimeout('avisar("Iván", 25)', 500); // ✗ mai: és eval encobertLa segona línia és l'error clàssic: els parèntesis criden la funció, així que setTimeout rep el valor retornat (normalment undefined) en lloc d'una funció. És el mateix descuit de 03-02 en passar funcions com a argument.
setInterval, clearTimeout i clearInterval
setInterval, clearTimeout i clearIntervalsetInterval repeteix el callback cada N mil·lisegons, indefinidament, fins que es cancel·li.
'use strict';
let ronda = 0;
const idInterval = setInterval(() => {
ronda += 1;
console.log(`Comprovant tasques vençudes… ronda ${ronda}`);
if (ronda === 3) {
clearInterval(idInterval); // ← imprescindible: sense això, continua per sempre
console.log('Comprovació aturada');
}
}, 1000);I clearTimeout cancel·la un setTimeout que encara no s'ha disparat:
const idAvis = setTimeout(() => {
console.log('⚠ El pressupost de la fusteria fa 3 dies que és vençut');
}, 5000);
// Si la tasca es completa abans, l'avís ja no té sentit
tauler.canviarEstat(6, 'en-curs');
clearTimeout(idAvis); // el callback no s'executarà mai| Funció | Què fa | Cancel·lar amb |
|---|---|---|
setTimeout(f, ms) |
Executa f una vegada, passats com a mínim ms |
clearTimeout(id) |
setInterval(f, ms) |
Executa f cada ms, sense fi |
clearInterval(id) |
Un avís sobre setInterval que costa car de descobrir en producció: no espera que el callback acabi. Si programes un interval de 100 ms i el callback triga 150 ms, les execucions se superposen i s'acumulen. Per a feines de durada variable —consultar un servidor, per exemple— és més segur un setTimeout que es reprograma a si mateix quan ha acabat:
/** Repetició segura: cada cicle comença quan l'anterior ha acabat. */
function comprovarPeriodicament(interval) {
setTimeout(function cicle() {
revisarVencudes(); // per llarga que sigui…
setTimeout(cicle, interval); // …el cicle següent es programa després
}, interval);
}I una regla operativa que t'estalviarà fuites de memòria (Mòdul 9): guarda sempre l'identificador i cancel·la quan la feina deixa de tenir sentit. Un interval oblidat continua corrent, consumint bateria i mantenint vives per referència totes les variables que el seu callback captura.
- Per què
setTimeout(f, 0) no és immediat
setTimeout(f, 0) no és immediatAquesta és una de les línies més incompreses del llenguatge:
'use strict';
console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');
// Sortida: A, C, BAmb un retard de zero mil·lisegons, B continua sortint l'últim. El motiu és que el segon argument no és «quan s'executarà», sinó «el temps mínim que ha de passar abans que sigui candidat a executar-se». I per ser candidat cal alguna cosa més: que la pila de crides estigui buida.
El mecanisme complet, en tres passos:
setTimeoutlliura el callback a l'entorn (el navegador o Node), que engega un temporitzador. Això no ocupa el fil.- Complert el temps, l'entorn col·loca el callback en una cua de tasques pendents.
- Quan el codi que s'està executant acaba del tot i la pila queda buida, s'agafa el primer de la cua i s'executa.
Per això un setTimeout(f, 0) darrere d'un bucle pesat no s'executa fins que el bucle acaba:
setTimeout(() => console.log('Hauria de sortir de seguida'), 0);
let suma = 0;
for (let i = 0; i < 500_000_000; i++) suma += i; // uns quants segons
console.log('Bucle acabat');
// Bucle acabat
// Hauria de sortir de seguida ← va esperar que la pila es buidésHi ha a més un detall històric: per especificació, els temporitzadors imbricats a partir de certa profunditat s'eleven a un mínim de 4 ms als navegadors. Així que setTimeout(f, 0) és realment «tan aviat com sigui possible, i com a mínim d'aquí a uns mil·lisegons, quan el fil estigui lliure».
Aquell ús —«executa això després del que estic fent ara»— és perfectament legítim i s'anomena cedir el fil. Tot el mecanisme de cues que acabes d'entreveure té nom propi, un algorisme precís i uns quants matisos més, i és el tema complet d'El Bucle d'Esdeveniments i la Cua de Microtasques. De moment en tens prou amb la regla: primer acaba tot el codi síncron; després, els callbacks pendents.
- El patró callback
Un callback és una funció que passes a una altra perquè la cridi més tard. La idea la coneixes des de 03-06: és exactament el que feies amb map, filter i sort. La diferència és quan es crida.
| Tipus | Exemple | Quan s'executa el callback |
|---|---|---|
| Síncron | [1,2,3].map((n) => n * 2) |
Immediatament, dins de la crida. map no torna fins que acaba |
| Asíncron | setTimeout(() => …, 1000) |
Més tard, quan la pila estigui lliure. La funció torna a l'instant |
Un callback asíncron es reconeix per un senyal inequívoc: la funció que el rep no retorna el resultat. Compara:
// Síncron: el resultat surt per return
function cercarTasca(backlog, id) {
return backlog.find((t) => t.id === id) ?? null;
}
const tasca = cercarTasca(backlog, 6);
// Asíncron: el resultat surt pel callback
function cercarTascaAlServidor(id, callback) {
setTimeout(() => {
callback(backlog.find((t) => t.id === id) ?? null);
}, 300);
// no hi ha return: la funció acaba aquí, sense resultat
}
cercarTascaAlServidor(6, (tasca) => console.log(tasca.titol));D'aquí surt la regla que regeix tota aquesta lliçó:
Un valor produït de manera asíncrona no es pot retornar amb
return. Només es pot lliurar a algú que espera rebre'l.
I d'aquí també l'error més freqüent de qui comença:
function cercarTascaMalament(id) {
let resultat = null;
setTimeout(() => { resultat = backlog.find((t) => t.id === id); }, 300);
return resultat; // ✗ SEMPRE null: el return passa 300 ms abans
}
console.log(cercarTascaMalament(6)); // nullNo hi ha cap manera d'arreglar aquella funció mantenint la signatura. El valor encara no existeix quan s'executa el return. Cal canviar el disseny, no el codi.
- La convenció error-first
Si el resultat viatja pel callback, els errors també ho han de fer. Node.js va establir una convenció que es va adoptar a tot arreu: el callback rep l'error com a primer argument i el resultat com a segon.
- Si tot ha anat bé:
callback(null, dades). - Si alguna cosa ha fallat:
callback(new Error('…')), sense segon argument.
'use strict';
import { ErrorDeDades } from '../model/errors.js';
/** Cerca una tasca al "servidor" (simulat). Convenció error-first. */
function cercarTascaAlServidor(id, callback) {
setTimeout(() => {
if (typeof id !== 'number') {
callback(new ErrorDeDades(`L'id ha de ser un nombre, rebut: ${typeof id}`));
return; // ← imprescindible: sense ell continuaria
}
const tasca = dadesBacklog.find((t) => t.id === id);
if (tasca === undefined) {
callback(new ErrorDeDades(`No existeix la tasca ${id}`));
return;
}
callback(null, tasca); // èxit: primer argument null
}, 300);
}
cercarTascaAlServidor(6, (error, tasca) => {
if (error) { // 1 · comprovar l'error SEMPRE, i primer
console.error(`✗ ${error.message}`);
return;
}
console.log(`✓ ${tasca.titol}`); // 2 · aquí ja se sap que hi ha resultat
});
// ✓ Pressupost de la fusteria
cercarTascaAlServidor(99, (error, tasca) => {
if (error) { console.error(`✗ ${error.message}`); return; }
console.log(`✓ ${tasca.titol}`);
});
// ✗ No existeix la tasca 99Tres detalls que cal respectar a ratlla:
- Comprovar l'error el primer de tot. Si et saltes aquell
if,tascaseràundefinedi l'error es transformarà en unTypeErrorconfús tres línies més avall. returndesprés de cridar el callback amb error. És el mateixreturnde guarda que vas aprendre a 02-01: sense ell, la funció continua i pot acabar cridant el callback dues vegades, cosa que trenca qui el consumeix.- Cridar el callback exactament una vegada. Ni zero (el consumidor es queda esperant per sempre, sense cap error) ni dues.
- Simular la càrrega del backlog
Amb tot l'anterior, ja podem substituir el crearBacklog() instantani de 05-04 per una versió que es comporti com el món real: amb latència i amb possibilitat de fallada.
// js/dades/backlogSimulat.js
import { Tasca } from '../model/tasca.js';
import { ErrorDeDades } from '../model/errors.js';
import { dadesBacklog } from './backlog.js';
/**
* Simula la càrrega del backlog des d'un servidor.
* Al Mòdul 7 això serà una crida real amb fetch (07-02);
* aquí la latència es fingeix amb setTimeout.
*
* @param {Function} callback (error, tasques) => void
* @param {Object} [opcions]
* @param {number} [opcions.latencia=400] mil·lisegons d'espera simulada
* @param {boolean} [opcions.fallar=false] força un error, per provar el camí dolent
*/
export function llegirBacklogSimulat(callback, opcions = {}) {
const { latencia = 400, fallar = false } = opcions;
setTimeout(() => {
if (fallar) {
callback(new ErrorDeDades('El servidor de Taller Nómada no respon (503).'));
return;
}
try {
const tasques = dadesBacklog.map((dades) => new Tasca(dades));
callback(null, tasques);
} catch (error) {
callback(new ErrorDeDades('El backlog rebut no és vàlid.', error));
}
}, latencia);
}I el seu ús des d'app.js:
import { llegirBacklogSimulat } from './dades/backlogSimulat.js';
import { Tauler } from './model/tauler.js';
import { AVUI } from './util/dates.js';
console.log('⏳ Carregant el backlog…');
llegirBacklogSimulat((error, tasques) => {
if (error) {
console.error(`✗ ${error.message}`);
return;
}
const tauler = new Tauler('Taller Nómada', tasques);
console.log('✓ Backlog carregat');
console.log(tauler.resum(AVUI));
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }
});
console.log("L'aplicació continua responent mentre carrega");
// Sortida:
// ⏳ Carregant el backlog…
// L'aplicació continua responent mentre carrega
// ✓ Backlog carregat
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }Fixa't en l'ordre: el missatge de «continua responent» surt abans que les dades. Aquesta és la prova que el fil va quedar lliure. I observa també un detall de disseny: el try/catch està dins del setTimeout, envoltant la construcció de les tasques, i converteix qualsevol ErrorDeValidacio del constructor en una crida al callback amb error. El motiu que hagi d'estar allà dins el veuràs a l'apartat 11.
Recordatori de progressió:
llegirBacklogSimulatfingeix la xarxa ambsetTimeout. L'API de debò —fetch, codis d'estat HTTP, capçaleres i tota la resta— arriba a 07-02, i la manera robusta de gestionar-ne les fallades i els temps d'espera, a 07-03. Aquí el que importa és la forma del codi asíncron, no d'on surten les dades.
- Encadenar tres operacions: la piràmide
Fins aquí, els callbacks semblen raonables. El problema apareix quan una operació asíncrona depèn del resultat d'una altra. La Marta demana un informe setmanal que requereix tres passos, i cadascun necessita el que va produir l'anterior:
- Carregar el backlog.
- Amb els responsables que apareguin, carregar les seves hores contractades.
- Amb les dues coses, generar l'informe i desar-lo.
Afegim els dos mòduls simulats que falten:
// js/dades/equipSimulat.js
import { ErrorDeDades } from '../model/errors.js';
const CONTRACTES = { 'Iván': 30, 'Marta': 20, 'Lucía': 35 }; // hores setmanals contractades
export function llegirHoresEquipSimulat(noms, callback, latencia = 300) {
setTimeout(() => {
const hores = {};
for (const nom of noms) {
if (!Object.hasOwn(CONTRACTES, nom)) {
callback(new ErrorDeDades(`${nom} no figura a l'equip de Taller Nómada.`));
return;
}
hores[nom] = CONTRACTES[nom];
}
callback(null, hores);
}, latencia);
}
export function desarInformeSimulat(informe, callback, latencia = 200) {
setTimeout(() => {
if (informe.linies.length === 0) {
callback(new ErrorDeDades('No es desa un informe buit.'));
return;
}
callback(null, { desat: true, id: `inf-${Date.now()}`, linies: informe.linies.length });
}, latencia);
}I ara l'informe, escrit amb callbacks:
import { llegirBacklogSimulat } from './dades/backlogSimulat.js';
import { llegirHoresEquipSimulat, desarInformeSimulat } from './dades/equipSimulat.js';
import { Tauler } from './model/tauler.js';
import { AVUI } from './util/dates.js';
console.log("⏳ Generant l'informe setmanal…");
llegirBacklogSimulat((errorBacklog, tasques) => {
if (errorBacklog) {
console.error(`✗ Error en carregar el backlog: ${errorBacklog.message}`);
return;
}
const tauler = new Tauler('Taller Nómada', tasques);
const carrega = tauler.horesPerResponsable();
const noms = Object.keys(carrega);
llegirHoresEquipSimulat(noms, (errorEquip, contractades) => {
if (errorEquip) {
console.error(`✗ Error en carregar l'equip: ${errorEquip.message}`);
return;
}
const linies = noms.map((nom) => ({
nom,
assignades: carrega[nom],
contractades: contractades[nom],
sobrecarrega: carrega[nom] > contractades[nom]
}));
desarInformeSimulat({ data: AVUI, linies }, (errorDesar, rebut) => {
if (errorDesar) {
console.error(`✗ Error en desar: ${errorDesar.message}`);
return;
}
console.log(`✓ Informe ${rebut.id} desat amb ${rebut.linies} línies`);
for (const l of linies) {
const marca = l.sobrecarrega ? '⚠' : '·';
console.log(` ${marca} ${l.nom.padEnd(8)} ${l.assignades} h assignades / ${l.contractades} h contractades`);
}
});
});
});La sortida és correcta:
⏳ Generant l'informe setmanal… ✓ Informe inf-1758... desat amb 3 línies · Iván 25 h assignades / 30 h contractades · Marta 6 h assignades / 20 h contractades · Lucía 14 h assignades / 35 h contractades
Però mira la forma del codi. Cada operació fica la següent un nivell més endins, i el tancament final és aquella escala de }); que s'ha guanyat el seu propi malnom:
flowchart TD
A["llegirBacklogSimulat((e, tasques) => {"] --> B[" llegirHoresEquipSimulat((e, hores) => {"]
B --> C[" desarInformeSimulat((e, rebut) => {"]
C --> D[" console.log(…)"]
D --> E[" });"]
E --> F[" });"]
F --> G["});"]
Això s'anomena callback hell o piràmide de la perdició. Amb tres passos encara es llegeix; amb sis —carregar, validar, enriquir, calcular, desar, notificar— és il·legible. I només són tres passos seqüencials: si a més calgués fer dues coses en paral·lel i esperar-les totes dues, hauries d'inventar-te un comptador manual.
- Els quatre problemes dels callbacks
La sagnia és el que es veu, però no és el més greu. Els problemes reals són quatre.
Problema 1: gestió d'errors repetida. Compta els blocs if (error) { console.error(...); return; } de l'exemple anterior: tres, un per nivell, pràcticament idèntics. No hi ha manera d'escriure «si alguna cosa falla en qualsevol punt d'aquesta seqüència, fes això». Cada nivell es defensa sol, i n'hi ha prou d'oblidar un if perquè una fallada passi desapercebuda i rebenti més endavant amb un missatge que no hi té res a veure.
Problema 2: l'ordre de lectura no és l'ordre d'execució. El codi es llegeix de dalt a baix però s'executa en un ball de salts. El que passa després del desarInformeSimulat és dins d'ell, i el que passa després de tot és al nivell més profund. El nostre cervell llegeix seqüències; això és un arbre.
Problema 3: inversió de control. Quan escrius desarInformeSimulat(informe, elMeuCallback), estàs lliurant la teva funció a un tercer. A partir d'aquí, ja no controles res:
| El que pot fer malament qui rep el teu callback | Conseqüència |
|---|---|
| No cridar-lo mai | El teu programa es queda penjat, sense error ni pista |
| Cridar-lo dues vegades | L'informe es desa dues vegades, el comptador es duplica |
| Cridar-lo massa aviat (síncronament) | Ordre d'execució impredictible segons el cas |
| Cridar-lo amb els arguments a l'inrevés | Tractes un error com si fos el resultat |
| Empassar-se una excepció del teu callback | Les fallades desapareixen en silenci |
Amb una funció teva no passa; amb una llibreria de tercers, tot això passa de debò. I no tens cap eina per protegir-te, tret de llegir-ne el codi.
Problema 4: compondre és impossible. Amb funcions normals, combinar és trivial: pipe(filtrar, ordenar, resumir), com vas fer a 03-06. Amb callbacks no hi ha res equivalent. Operacions tan comunes com «fes aquestes tres coses alhora i avisa'm quan totes acabin» o «reintenta això tres vegades si falla» requereixen escriure a mà comptadors, banderes i comprovacions:
// "Executar dues càrregues en paral·lel i continuar quan totes dues acabin", a mà
let pendents = 2;
let resultatA = null;
let resultatB = null;
let jaHaFallat = false;
function comprovarSiHemAcabat() {
pendents -= 1;
if (pendents === 0 && !jaHaFallat) continuar(resultatA, resultatB);
}
carregarA((error, dades) => {
if (error) { jaHaFallat = true; return gestionar(error); }
resultatA = dades;
comprovarSiHemAcabat();
});
carregarB((error, dades) => {
if (error) { jaHaFallat = true; return gestionar(error); }
resultatB = dades;
comprovarSiHemAcabat();
});Quatre variables d'estat i una funció auxiliar per expressar una idea de mitja línia. A la lliçó següent això serà Promise.all([carregarA(), carregarB()]).
try/catch no captura errors asíncrons
try/catch no captura errors asíncronsA 02-05 va quedar anotat un avís que ara pots entendre del tot: try/catch no captura errors que passen dins d'un callback asíncron.
'use strict';
try {
setTimeout(() => {
throw new Error('Error dins del temporitzador');
}, 100);
console.log('El try ha acabat sense problemes');
} catch (error) {
console.error('Capturat:', error.message); // ← MAI no s'executa
}
// Sortida:
// El try ha acabat sense problemes
// …100 ms després: Uncaught Error: Error dins del temporitzadorLa raó és exactament la de l'apartat 5, i ara encaixa amb el que saps de la pila de crides de 03-05. try/catch protegeix una regió de la pila: captura el que es llanci mentre aquelles línies s'estan executant. Quan el callback s'executa, cent mil·lisegons més tard, la pila que contenia el try ja no existeix; el callback arrenca sobre una pila nova i buida, sense cap try al voltant.
flowchart TD
subgraph T1["t = 0 ms · pila amb el try"]
A["try { … }"] --> B["setTimeout registra el callback"]
B --> C["el try acaba · la pila es buida"]
end
subgraph T2["t = 100 ms · pila nova"]
D["callback()"] --> E["throw Error"]
E --> F["⚠ ningú no el captura:<br/>aquí no hi ha cap try"]
end
C -.->|"el temps passa"| D
L'única manera de capturar-lo amb callbacks és posar el try/catch dins del callback, que és justament el que vam fer a llegirBacklogSimulat:
setTimeout(() => {
try {
const tasques = dadesBacklog.map((dades) => new Tasca(dades));
callback(null, tasques);
} catch (error) { // ✓ el try és a la mateixa pila que el throw
callback(new ErrorDeDades('El backlog rebut no és vàlid.', error));
}
}, latencia);I aquí hi ha la conseqüència de disseny més important de tota la lliçó: amb callbacks, els errors no es propaguen sols. Al codi síncron de 02-05, un throw al fons de deu crides pujava per la pila fins al primer try/catch, sense que les funcions intermèdies fessin res. Amb callbacks, cada nivell ha de capturar el seu error i passar-lo a mà al callback de dalt. El mecanisme de propagació automàtica del llenguatge, que era una de les seves millors característiques, deixa de funcionar així que travesses una frontera asíncrona.
- Mitigacions parcials i per què no en tenen prou
Existeixen tècniques per alleujar la piràmide. Val la pena conèixer-les perquè el codi antic les fa servir, i perquè entendre'n els límits explica per què va caldre un mecanisme nou.
Mitigació 1: anomenar les funcions i aplanar. En lloc d'imbricar fletxes anònimes, es declaren funcions amb nom i es passen per referència.
'use strict';
let taulerActual = null;
let carregaActual = null;
function enCarregarBacklog(error, tasques) {
if (error) return abortar('carregar el backlog', error);
taulerActual = new Tauler('Taller Nómada', tasques);
carregaActual = taulerActual.horesPerResponsable();
llegirHoresEquipSimulat(Object.keys(carregaActual), enCarregarEquip);
}
function enCarregarEquip(error, contractades) {
if (error) return abortar("carregar l'equip", error);
const linies = Object.keys(carregaActual).map((nom) => ({
nom, assignades: carregaActual[nom], contractades: contractades[nom]
}));
desarInformeSimulat({ data: AVUI, linies }, enDesarInforme);
}
function enDesarInforme(error, rebut) {
if (error) return abortar("desar l'informe", error);
console.log(`✓ Informe ${rebut.id} desat`);
}
function abortar(fase, error) {
console.error(`✗ Error en ${fase}: ${error.message}`);
}
llegirBacklogSimulat(enCarregarBacklog);És millor: la sagnia desapareix, cada funció té nom i les traces d'error de 03-05 són llegibles. Però mira què ha costat:
- Han aparegut variables compartides (
taulerActual,carregaActual) per passar dades entre passos. És estat global reintroduït per la porta del darrere, amb tots els problemes que vas aprendre a evitar a 03-04. - La seqüència ja no es llegeix enlloc. Per saber què passa després de carregar el backlog cal buscar l'última línia d'
enCarregarBacklog, anar aenCarregarEquip, i així successivament. L'ordre està repartit pel fitxer. - La gestió d'errors continua repetida a cada funció, encara que ara delegui en
abortar.
Mitigació 2: modularitzar. Ficar cada pas al seu propi mòdul (05-04) millora l'organització, però no canvia res de l'anterior: els problemes són de forma del control de flux, no d'on viu el codi.
Mitigació 3: llibreries de control de flux. Cap al 2012 es van popularitzar llibreries com async amb funcions tipus waterfall, series i parallel:
// Estil de llibreria de control de flux (il·lustratiu)
serie([carregarBacklog, carregarEquip, desarInforme], (error, resultats) => { … });Funcionaven, però eren una convenció externa: calia aprendre-les, no formaven part del llenguatge, i la inversió de control del problema 3 continuava intacta, perquè continuaves lliurant les teves funcions a un tercer.
Cap mitigació no toca els dos problemes de fons:
| Problema | Ho resol anomenar/modularitzar? |
|---|---|
| Sagnia en piràmide | Sí |
| Errors repetits a cada nivell | No |
try/catch inútil sobre el que és asíncron |
No |
| Inversió de control | No |
| Compondre (paral·lel, reintents, timeouts) | No |
El que cal és que una operació asíncrona retorni alguna cosa: un objecte que representi «el resultat que arribarà», que es pugui guardar en una variable, passar a una funció, encadenar i combinar, i que propagui els errors automàticament com feia la pila síncrona. Aquell objecte existeix, s'anomena promesa, i és el tema de la lliçó següent.
Errors Habituals i Consells
- Intentar
returndes d'un callback asíncron. El valor no existeix quan s'executa elreturn. Si et veus escrivintlet resultat; setTimeout(() => resultat = …); return resultat;, atura't: el disseny ha de canviar, no el codi. - Cridar la funció en lloc de passar-la.
setTimeout(avisar(), 100)executaavisarimmediatament. Es passaavisaro() => avisar(...). - Oblidar el
returndesprés de cridar el callback amb error. La funció continua i acaba cridant el callback una segona vegada amb dades incompletes. És una fallada endimoniada de depurar. - No comprovar l'error, o comprovar-lo després de fer servir el resultat. La primera línia del callback ha de ser
if (error) { …; return; }. - Embolcallar una crida asíncrona en
try/catchesperant capturar alguna cosa. No funciona: elcatchpertany a una pila que ja no existeix. Eltryva dins del callback. - Oblidar
clearInterval. Un interval orfe corre per sempre, gasta bateria i manté vives per referència totes les variables que captura: és una fuita de memòria de manual (Mòdul 9). - Confiar en el valor exacte del retard.
setTimeout(f, 100)significa «no abans de 100 ms», mai «exactament als 100 ms». Per mesurar temps real, fes servir marques ambDate.now()en lloc de comptar temporitzadors. - Bucles amb temporitzadors a dins.
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0)imprimeix0 1 2ambletperò3 3 3ambvar: és exactament el cas d'àmbit per bloc de 03-04, ara amb conseqüències asíncrones. - Consell: anomena els callbacks pel que passa, no pel que fan:
enCarregarBacklog,enFallarDesat. Quan apareguin en una traça d'error a les tres de la matinada, ho agrairàs.
Exercicis
Exercici 1 — Predir la sortida. Sense executar-lo, escriu l'ordre exacte en què apareixen les sis línies i justifica cadascuna.
console.log('1');
setTimeout(() => console.log('2'), 0);
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(`3.${i}`), 100 - i * 50);
}
console.log('4');Exercici 2 — comptarTasquesPerResponsableSimulat. Escriu una funció amb la convenció error-first que rebi (responsable, callback) i, després de 250 ms simulats, lliuri { responsable, obertes, hores } amb les dades del backlog canònic. Ha de fallar amb ErrorDeDades si el responsable no existeix o si no és un string. Prova-la amb 'Iván' (ha de donar 3 tasques obertes i 25 h), amb 'Ningú' i amb 42.
Exercici 3 — Reintents amb callbacks. Escriu ambReintents(operacio, intents, callback) que executi una operació asíncrona amb estil error-first i, si falla, la reintenti fins a intents vegades abans de rendir-se, esperant 200 ms entre intents. Prova-la amb una operació que falli les dues primeres vegades i funcioni a la tercera. Després respon: quantes línies t'ha costat i quina part és lògica de reintent i quina és fontaneria de callbacks?
Solucions
Exercici 1
1 · síncron 4 · síncron: tot el codi de nivell superior s'executa primer 3.2 · retard 0 ms (100 - 2*50) 2 · retard 0 ms, però es va REGISTRAR abans que 3.2… vegeu la nota 3.1 · retard 50 ms 3.0 · retard 100 ms
El punt interessant és l'ordre entre 2 i 3.2. Tots dos tenen retard 0, i quan dos temporitzadors vencen alhora s'executen en l'ordre en què es van registrar, així que a la pràctica veuràs 2 abans que 3.2:
Dues lliçons. La primera: tot el codi síncron va primer, sense excepció; 4 surt abans que qualsevol temporitzador encara que aquests tinguin retard zero. La segona: els callbacks no surten en l'ordre en què es van escriure, sinó per temps de venciment; el bucle registra els retards decreixents 100, 50 i 0, així que la sortida va a l'inrevés que el bucle. I observa que i val el que toca a cadascun gràcies a let, que crea una variable nova per volta (03-04); amb var hauries obtingut tres 3.3.
Exercici 2
'use strict';
import { dadesBacklog } from './dades/backlog.js';
import { ErrorDeDades } from './model/errors.js';
/**
* Compta les tasques obertes i les hores d'un responsable.
* @param {string} responsable
* @param {Function} callback (error, resum) => void
*/
export function comptarTasquesPerResponsableSimulat(responsable, callback) {
setTimeout(() => {
if (typeof responsable !== 'string') {
callback(new ErrorDeDades(`El responsable ha de ser un string, rebut: ${typeof responsable}`));
return;
}
const seves = dadesBacklog.filter((t) => t.responsable === responsable);
if (seves.length === 0) {
callback(new ErrorDeDades(`${responsable} no té tasques al backlog.`));
return;
}
const obertes = seves.filter((t) => t.estat !== 'feta');
callback(null, {
responsable,
obertes: obertes.length,
hores: obertes.reduce((s, t) => s + t.horesEstimades, 0)
});
}, 250);
}
function mostrar(error, resum) {
if (error) { console.error(`✗ ${error.message}`); return; }
console.log(`✓ ${resum.responsable}: ${resum.obertes} obertes, ${resum.hores} h`);
}
comptarTasquesPerResponsableSimulat('Iván', mostrar); // ✓ Iván: 3 obertes, 25 h
comptarTasquesPerResponsableSimulat('Ningú', mostrar); // ✗ Ningú no té tasques al backlog.
comptarTasquesPerResponsableSimulat(42, mostrar); // ✗ El responsable ha de ser un string, rebut: numberLes 25 h de l'Iván són les canòniques: 12 de la sala polivalent, 8 de la guia d'enquadernació i 5 del pressupost de la fusteria. Fixa't en les tres crides seguides: totes tres es registren immediatament, sense que cap esperi l'anterior, i els seus resultats arriben als 250 ms més o menys alhora. Això és paral·lelisme d'operacions asíncrones, de franc, sense cap coordinació. El problema —com vas veure a l'apartat 10— apareix quan necessites saber quan han acabat totes.
Exercici 3
'use strict';
/**
* Executa una operació error-first reintentant-la si falla.
* @param {Function} operacio (callback) => void
* @param {number} intents nombre màxim d'intents
* @param {Function} callback (error, resultat) => void
*/
function ambReintents(operacio, intents, callback) {
let restants = intents;
function intentar() {
operacio((error, resultat) => {
if (!error) {
callback(null, resultat);
return;
}
restants -= 1;
if (restants === 0) {
callback(new Error(`Han fallat els ${intents} intents. Últim error: ${error.message}`));
return;
}
console.log(` ↻ reintentant… en queden ${restants}`);
setTimeout(intentar, 200);
});
}
intentar();
}
// Operació de prova: falla les dues primeres vegades
let vegades = 0;
function servidorInestable(callback) {
vegades += 1;
const intent = vegades;
setTimeout(() => {
if (intent < 3) callback(new Error(`503 a l'intent ${intent}`));
else callback(null, { backlog: 6, hores: 48 });
}, 100);
}
ambReintents(servidorInestable, 4, (error, dades) => {
if (error) { console.error(`✗ ${error.message}`); return; }
console.log('✓ Rebut:', dades);
});
// Sortida:
// ↻ reintentant… en queden 3
// ↻ reintentant… en queden 2
// ✓ Rebut: { backlog: 6, hores: 48 }Responent a la pregunta de l'enunciat: són unes vint línies, i només tres expressen la idea de reintent (restants -= 1, la comprovació d'esgotament i el setTimeout(intentar, 200)). Tota la resta és fontaneria: la funció interna intentar que existeix només per poder repetir-se, la comprovació manual de l'error, els return de guarda després de cada crida al callback i el tancament de la imbricació. I hi ha una fragilitat amagada: si operacio cridés el seu callback dues vegades —el problema 3 de l'apartat 10—, ambReintents cridaria dues vegades el seu, i res en aquest codi no ho impedeix.
Guarda aquest exercici. Quan a la lliçó següent el reescriguis amb promeses i async/await, la lògica de reintent cabrà en un for de cinc línies amb un try/catch normal, i el problema de la doble crida desapareixerà per construcció.
Conclusió
Has entès el problema de fons i les eines de primera generació per resoldre'l. JavaScript té un sol fil, cosa que li estalvia tota una categoria d'errors de concurrència però li imposa una regla implacable: mentre una funció s'executa, res més no ho pot fer. Bloquejar tres segons no significa «trigar tres segons», significa congelar l'aplicació sencera durant tres segons. La sortida no és esperar millor, sinó no esperar: demanar la dada, declarar què fer quan arribi i tornar el control immediatament. Per això la sortida d'un programa asíncron desconcerta al principi —'Llest' apareix abans que les dades—, i per això aquella inversió d'ordre és precisament el senyal que el fil ha quedat lliure.
Domines les peces bàsiques. setTimeout per programar una execució futura, amb el seu identificador i el seu clearTimeout; setInterval per repetir, amb l'advertiment que no espera que el callback acabi i que un interval oblidat és una fuita de memòria; i l'explicació de per què setTimeout(f, 0) no és immediat: el retard és un mínim, i a més el callback ha d'esperar que la pila de crides es buidi. Saps reconèixer un callback asíncron per un senyal inequívoc —la funció que el rep no retorna el resultat—, i d'aquí la regla que ho governa tot: un valor produït de manera asíncrona no es pot retornar amb return, només lliurar. I coneixes la convenció error-first de Node, (error, resultat), amb les seves tres disciplines: comprovar l'error el primer, posar return després de cada crida al callback, i cridar-lo exactament una vegada.
Amb això has construït llegirBacklogSimulat, llegirHoresEquipSimulat i desarInformeSimulat, has carregat el backlog canònic amb latència fingida —48 h, 45 obertes, 1 vençuda, esforç 124, amb l'aplicació responent mentrestant— i has encadenat els tres passos de l'informe setmanal de la Marta fins a veure la piràmide amb els teus propis ulls. I has diagnosticat per què fa mal, que és l'important: la gestió d'errors es repeteix a cada nivell, l'ordre de lectura deixa de coincidir amb el d'execució, lliurar la teva funció a un tercer és una inversió de control sense garanties —pot no cridar-la, cridar-la dues vegades o empassar-se les seves excepcions—, i compondre operacions (dues en paral·lel, un reintent, un temps màxim) exigeix inventar comptadors i banderes a mà. Per damunt de tot, has confirmat l'avís que va quedar pendent a 02-05: try/catch no captura errors asíncrons, perquè quan el callback s'executa la pila que contenia el try ja no existeix. La propagació automàtica d'errors, una de les millors característiques del llenguatge, deixa de funcionar així que travesses una frontera asíncrona.
Les mitigacions —anomenar les funcions, aplanar la piràmide, modularitzar, fer servir llibreries de control de flux— arreglen la sagnia i poca cosa més, i a sobre reintrodueixen variables compartides per passar dades entre passos. El problema no era estètic. El que falta és que una operació asíncrona retorni un valor: un objecte que representi el resultat futur, que es pugui guardar en una variable, passar a una funció, encadenar sense imbricar, combinar en paral·lel i, sobretot, que propagui els errors per si mateix com feia la pila síncrona. Aquell objecte existeix des d'ES2015, s'anomena promesa, i amb la sintaxi async/await que es va construir al damunt permet escriure codi asíncron que es llegeix exactament igual que el síncron —try/catch inclòs—. És el tema de Promeses i Async/Await, on tornaràs a escriure l'informe setmanal de la Marta i comprovaràs quant de codi desapareix.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
