El Mòdul 7 va acabar amb un diagnòstic incòmode: Nómada Tasques té sis capes, catorze mòduls, dues fonts de dades i un canal en temps real, i l'única manera de saber si alguna cosa funciona és obrir-la i provar-la a mà. Aquesta lliçó ataca la primera meitat del problema —quan ja saps que alguna cosa falla, com esbrines per què?— i ho fa amb un canvi de mentalitat: depurar no és endevinar, és un procediment. Aprendràs el cicle reproduir → aïllar → formular hipòtesis → comprovar → corregir → prevenir, la tècnica de bisecció que redueix a la meitat el terreny sospitós a cada pas, la consola sencera (que és molt més que console.log), el depurador de les DevTools amb les seves sis classes de punts d'interrupció, la lectura d'una pila de crides asíncrona, els source maps, i els panells de xarxa i emmagatzematge. I acabaràs resolent tres errors reals de Nómada Tasques pas a pas, amb el mètode al davant.
Contingut
- Per què depurar per intuïció no escala
- El mètode: sis passos
- Bisecció: partir el problema per la meitat
- La consola més enllà de
console.log - El truc de les claus:
console.log({variable}) - La sentència
debuggeri el primer punt d'interrupció - Els sis tipus de punt d'interrupció
- Executar pas a pas: step over, into i out
- Els panells Scope, Watch i Call Stack
- Llegir un stack trace i «Pause on exceptions»
- Depurar codi asíncron
- Source maps: per què producció és il·legible sense ells
- Depurar la xarxa: el panell Network
- Depurar l'emmagatzematge: el panell Application
- Depurar en un mòbil real
- Cas 1: la tasca que no canvia d'estat
- Cas 2: el filtre que perd tasques en recarregar
- Cas 3: el comptador d'hores descompensat
- El bug que no es reprodueix
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Per què depurar per intuïció no escala
La manera com gairebé tothom comença a depurar és aquesta: llegir el codi on creu que hi ha l'error, veure alguna cosa estranya, canviar-la, recarregar, mirar si continua fallant. Repetir. Això s'anomena depuració per canvi aleatori, i té tres problemes greus:
- No convergeix. Cada canvi modifica el sistema, així que l'error pot desaparèixer per una raó diferent de la que creies, i reaparèixer una setmana després.
- Introdueix errors nous. Un
ifafegit "per si de cas" que tapa el símptoma deixa el sistema amb dos problemes en lloc d'un. - No ensenya res. Quan l'error se'n va, no saps per què se n'ha anat, i per tant no pots evitar el següent de la mateixa família.
L'alternativa és tractar l'error com el que és: una diferència entre el que el programa fa i el que creus que fa. Depurar és localitzar el punt exacte on el teu model mental i la realitat se separen. I per a això existeix un procediment.
Una idea que convé interioritzar des d'ara: el 90 % del temps de depuració es gasta a localitzar l'error, no a arreglar-lo. La correcció sol ser una línia. Per això tot el mètode està orientat a localitzar de pressa, no a escriure de pressa.
- El mètode: sis passos
flowchart TD
A["1 · Reproduir<br/>passos exactes i fiables"] --> B["2 · Aïllar<br/>reduir al cas mínim"]
B --> C["3 · Hipòtesi<br/>una afirmació falsable"]
C --> D["4 · Comprovar<br/>breakpoint, registre o prova"]
D -->|hipòtesi falsa| C
D -->|hipòtesi certa| E["5 · Corregir<br/>la causa, no el símptoma"]
E --> F["6 · Prevenir<br/>una prova que falli sense l'arranjament"]
Els sis passos, amb el que significa cadascun a la pràctica:
| Pas | Què fas | Com saps que has acabat |
|---|---|---|
| 1 · Reproduir | Escrius la seqüència exacta d'accions que provoca l'error | Pots provocar-lo a voluntat, tres vegades seguides |
| 2 · Aïllar | Treus tot el que no sigui imprescindible: dades, mòduls, passos | Un cas mínim que continua fallant |
| 3 · Hipòtesi | Formules una frase concreta i comprovable: «id arriba com a cadena» |
La frase es pot demostrar falsa amb una dada |
| 4 · Comprovar | Poses un punt d'interrupció o un registre que la confirmi o la negui | Tens un valor observat, no una impressió |
| 5 · Corregir | Arregles la causa, a la capa correcta | El cas mínim passa i entens per què |
| 6 · Prevenir | Escrius una prova automàtica que falla sense l'arranjament | La prova es posa verda en aplicar la correcció |
El pas 6 és el que gairebé tothom es salta, i és el que converteix una tarda perduda en un actiu permanent. Encara no saps escriure aquestes proves —això arriba a Proves Unitàries amb Jest—, però en aquesta lliçó ja deixaràs escrita la comprovació que hauria d'existir per a cada error. Quan arribis a 08-03 tindràs tres proves esperant-te.
Una regla d'or del pas 3: una hipòtesi que no es pot demostrar falsa no és una hipòtesi. «Crec que hi ha alguna cosa estranya al filtre» no serveix. «Crec que estat.filtres.responsable val 'Iván' quan hauria de valer null» sí que serveix, perquè una sola ullada la resol.
- Bisecció: partir el problema per la meitat
Quan l'error és en algun punt d'un recorregut llarg —el clic entra per controlador.js, passa per tauler.js, torna per tauler-vista.js i acaba a repositori-local.js—, buscar llegint és lent. La tècnica correcta és la bisecció: triar un punt intermedi del recorregut, comprovar si allà les dades ja estan malament, i descartar la meitat corresponent.
flowchart LR
A["Clic<br/>controlador"] --> B["Tauler<br/>canviarEstat"]
B --> C["Esdeveniment<br/>tasca:canviada"]
C --> D["Vista<br/>reconciliar"]
D --> E["Repositori<br/>desar"]
style C fill:#fde68a,stroke:#b45309
Si la dada ja està malament al punt mitjà (C), l'error és entre A i C. Si està bé, és entre C i E. Cada comprovació divideix el terreny entre dos. Amb quatre comprovacions cobreixes un recorregut de setze passos.
La bisecció s'aplica a tres dimensions diferents, i convé conèixer-les totes tres:
- Al flux de dades, com acabes de veure: comprovar en punts intermedis del camí.
- Al temps, amb
git bisect: si l'aplicació funcionava fa trenta commits i ara no, Git pot buscar en binari el commit culpable en cinc passos.
git bisect start
git bisect bad # el HEAD actual falla
git bisect good a455c72 # aquest commit funcionava
# Git et situa en un commit intermedi: proves i respons
git bisect good # o: git bisect bad
# ... cinc iteracions ...
git bisect reset # torna on eres- Al codi, comentant la meitat: si desactives
connectarFormularii l'error desapareix, ja saps de quin costat mirar. És la versió més tosca, però en una interfície amb molts oients és sorprenentment eficaç.
- La consola més enllà de
console.log
console.logconsole.log és una eina legítima —qui digui que un professional no la fa servir mai, menteix—, però l'objecte console té una dotzena de mètodes que resolen millor problemes concrets. Aquests són els que es fan servir de debò:
| Mètode | Per a què serveix | Exemple a Nómada Tasques |
|---|---|---|
console.table(dades) |
Mostra un array d'objectes com a taula ordenable | console.table(tauler.tasques.map((t) => t.toJSON())) |
console.dir(obj) |
Mostra l'objecte com a estructura, no com a text | console.dir(document.querySelector('#llista-tasques')) |
console.group() / groupEnd() |
Agrupa i plega registres relacionats | Un grup per cada render |
console.count(etiqueta) |
Compta quantes vegades es passa per un punt | Detectar renders duplicats |
console.time() / timeEnd() |
Mesura el temps entre dos punts | console.time('render') … console.timeEnd('render') |
console.assert(cond, msg) |
Registra només si la condició és falsa | console.assert(r.horesObertes === 45, r) |
console.trace() |
Imprimeix la pila de crides sense aturar res | Qui ha cridat desar()? |
console.warn / error |
Nivell de gravetat: es poden filtrar per nivell | Avisos del repositori |
Quatre d'ells mereixen un exemple complet, perquè canvien de debò la manera de treballar.
console.table és la diferència entre entendre un array de sis objectes d'una ullada i no entendre'l. Accepta un segon argument amb les columnes que vols:
// A la consola, amb l'aplicació oberta
console.table(
tauler.tasques.map((t) => t.toJSON()),
['id', 'titol', 'responsable', 'estat', 'horesEstimades']
);┌─────────┬────┬───────────────────────────────┬─────────────┬─────────────┬────────────────┐ │ (index) │ id │ titol │ responsable │ estat │ horesEstimades │ ├─────────┼────┼───────────────────────────────┼─────────────┼─────────────┼────────────────┤ │ 0 │ 1 │ 'Redissenyar la sala…' │ 'Iván' │ 'en-curs' │ 12 │ │ 1 │ 2 │ 'Cartelleria del taller…' │ 'Marta' │ 'pendent' │ 6 │ │ 2 │ 3 │ 'Actualitzar el web…' │ 'Lucía' │ 'pendent' │ 14 │ │ 3 │ 4 │ 'Inventari de tintes…' │ 'Marta' │ 'feta' │ 3 │ │ 4 │ 5 │ 'Guia sobre enquadernació…' │ 'Iván' │ 'en-curs' │ 8 │ │ 5 │ 6 │ 'Pressupost de la…' │ 'Iván' │ 'pendent' │ 5 │ └─────────┴────┴───────────────────────────────┴─────────────┴─────────────┴────────────────┘
Les columnes de la taula de DevTools s'ordenen en prémer la capçalera. Ordenar per estat i comptar és més ràpid que qualsevol filter escrit a mà.
console.count respon a la pregunta més freqüent d'una interfície: per què això s'executa tres vegades?
render del tauler: 1 render del tauler: 2 render del tauler: 3 ← un sol clic ha provocat tres renders
Aquest resultat ja és un diagnòstic: hi ha tres oients diferents reaccionant al mateix esdeveniment, o l'esdeveniment fa bombolla i es gestiona dues vegades. Sense count, aquest error es manifesta només com a "va una mica lent".
console.assert és una afirmació que només parla quan s'incompleix. És perfecta per vigilar invariants coneguts, com els números canònics del backlog:
const r = tauler.resum(AVUI);
console.assert(r.horesObertes === 45, 'Hores obertes descompensades', r);
console.assert(r.esforc === 124, 'Esforç ponderat descompensat', r);
// Si tot va bé no imprimeix res. Si falla:
// Assertion failed: Hores obertes descompensades {total: 6, obertes: 5, horesObertes: 48, …}console.group converteix un abocador de línies en un arbre plegable:
export function desarTauler(tauler) {
console.group(`[repositori] desar · ${tauler.total} tasques`);
console.log('clau:', 'nomada:tauler:v1');
console.table(tauler.resum(AVUI));
const ok = repositori.desar(tauler);
console.log('resultat:', ok ? 'desat' : 'sense espai');
console.groupEnd();
}Amb console.groupCollapsed() el grup apareix ja plegat, que és el que vols quan registres una cosa que passa moltes vegades.
- El truc de les claus:
console.log({variable})
console.log({variable})Aquest petit truc estalvia més temps del que sembla. Compara:
console.log(responsable, estat, visibles.length);
// Iván pendent 3 ← quin era quin?
console.log({ responsable, estat, visibles: visibles.length });
// {responsable: 'Iván', estat: 'pendent', visibles: 3} ← amb nomsEn envoltar les variables amb claus fas servir l'abreviatura de propietats d'ES6 (04-01): { responsable } és { responsable: responsable }. El resultat és un objecte on cada valor ve etiquetat amb el nom de la seva variable, i a més es mostra plegable i inspeccionable a la consola. Adopta'l com a norma: no costa res i elimina la classe sencera d'errors de "estic llegint el registre d'una altra variable".
Dos complements del mateix estil:
// Marcar el punt exacte, útil quan hi ha diversos registres iguals
console.log('[controlador:avancar]', { id, estatActual: tasca.estat });
// Copiar un valor gran al porta-retalls per enganxar-lo en un fitxer
copy(tauler.tasques.map((t) => t.toJSON())); // copy() només existeix a la consolacopy() no és JavaScript estàndard: és una de les funcions d'utilitat de la consola de DevTools, juntament amb $0 (l'últim element seleccionat a l'inspector), $_ (l'últim resultat) i $$('selector') (un querySelectorAll que retorna un array de debò). Només funcionen escrites a mà a la consola, mai al teu codi.
- La sentència
debugger i el primer punt d'interrupció
debugger i el primer punt d'interrupcióconsole.log et diu el valor del que se't va acudir imprimir. El depurador et deixa mirar-ho tot, en el moment exacte, i a més seguir avançant. La diferència de potència és enorme.
La manera més ràpida d'entrar al depurador és la paraula clau debugger:
canviarEstat(id, nou) {
const tasca = this.cercarPerId(id);
debugger; // ← el navegador s'atura AQUÍ
if (tasca === null) throw new ErrorDeValidacio(`No existeix la tasca ${id}.`, 'id', id);
tasca.canviarEstat(nou);
return this;
}Amb les DevTools obertes, l'execució es congela en aquesta línia i pots inspeccionar id, nou, this i tota la resta. Amb les DevTools tancades, la sentència no fa res.
debugger té un avantatge (és una línia, funciona sempre, sobreviu a un canvi de fitxer) i un perill seriós: si s'escola a producció, congela l'aplicació a qualsevol que tingui les eines obertes. A la lliçó següent configuraràs una regla d'ESLint (no-debugger) que impedeix exactament això.
L'habitual, però, és no tocar el codi: al panell Sources (Chrome/Edge) o Depurador (Firefox), obres el fitxer i prems el número de línia. Apareix un marcador blau: aquest és un punt d'interrupció normal.
- Els sis tipus de punt d'interrupció
Aquí és on el depurador deixa de ser "un console.log més elegant" i es converteix en una altra cosa. Aquests són els sis tipus i quan fer servir cadascun:
| Tipus | Com es posa | Quan és l'eina correcta |
|---|---|---|
| Normal | Clic al número de línia | Vols aturar-te sempre en aquest punt |
| Condicional | Clic dret → Add conditional breakpoint | La línia s'executa 200 vegades i només t'interessa un cas |
| De registre (logpoint) | Clic dret → Add logpoint | Vols el valor sense aturar i sense tocar el codi |
| Per esdeveniment | Event Listener Breakpoints → Mouse → click |
No saps quin codi gestiona aquest clic |
| Per petició | XHR/fetch Breakpoints → afegir /tasques |
Vols aturar-te just abans d'una petició concreta |
| Per canvi del DOM | Clic dret al node → Break on… | Un element canvia i no saps qui el canvia |
El condicional és el que més temps estalvia en aquest projecte. La funció pintarTargeta s'executa sis vegades per render; aturar-se a les sis és inútil. Amb la condició tasca.id === 3 t'atures només a la que t'interessa:
// Condició escrita al diàleg del breakpoint (no al codi):
tasca.id === 3 && tasca.estat === 'pendent'La condició és una expressió JavaScript normal avaluada a l'àmbit d'aquella línia. Truc poc conegut: si escrius console.count('pas') com a condició, no s'atura mai (perquè retorna undefined, que és fals) però sí que executa el comptador. Això és exactament el que fa un logpoint, que és la versió oficial de la idea.
El logpoint mereix un paràgraf propi perquè substitueix el 80 % dels console.log que la gent escriu:
// Al diàleg del logpoint, sobre la línia de canviarEstat:
'canviant', {id, tipus: typeof id, nou, estatActual: tasca?.estat}Avantatges respecte a escriure el console.log al fitxer: no modifiques el codi, no cal recarregar, no hi ha risc d'oblidar-lo en un commit, i funciona igual sobre codi de tercers o minificat. Quan acabes, esborres el punt i no queda rastre.
Els punts per esdeveniment resolen la pregunta "quin codi s'executa quan premo aquest botó?" en una aplicació que no coneixes. Actives Mouse → click, prems el botó, i el depurador et deixa dins del gestor, amb la pila de crides completa. A Nómada Tasques això et portaria directament a l'oient delegat de js/vista/controlador.js, amb esdeveniment.target ja disponible.
Els punts per petició (XHR/fetch Breakpoints) s'aturen just abans que surti una petició la URL de la qual contingui el text que indiquis —per exemple tasques—. Serveixen per inspeccionar el body que estàs a punt d'enviar abans que el servidor el rebutgi, i per descobrir qui dispara una petició inesperada: la pila de crides t'ho diu.
Els punts per canvi del DOM són la solució al clàssic "aquesta classe es treu sola". A l'inspector, clic dret sobre el node → Break on → attribute modifications, i el depurador s'atura a la línia de JavaScript que modifica aquell atribut. Hi ha tres variants:
| Variant | Es dispara quan |
|---|---|
| subtree modifications | S'afegeix o s'elimina un descendent (útil per a reconciliar) |
| attribute modifications | Canvia un atribut: class, data-estat, disabled, hidden |
| node removal | El mateix node s'elimina de l'arbre |
- Executar pas a pas: step over, into i out
Un cop aturat, controles l'execució amb quatre botons. Entendre'ls bé és el que separa fer servir el depurador de patir-lo:
| Acció | Drecera habitual | Què fa |
|---|---|---|
| Resume (continuar) | F8 | Segueix fins al proper punt d'interrupció |
| Step over (saltar) | F10 | Executa la línia sencera, sense entrar a les funcions que crida |
| Step into (entrar) | F11 | Entra dins de la funció que es crida en aquesta línia |
| Step out (sortir) | Maj + F11 | Acaba la funció actual i torna a qui l'ha cridada |
La regla pràctica és senzilla:
- Fes servir step over per defecte. Vas llegint el flux de la funció actual sense perdre't a les crides.
- Fes servir step into només quan sospitis de la funció que estàs cridant.
- Fes servir step out tan bon punt t'adonis que has entrat on no volies (típicament, dins d'una funció de llibreria).
Amb reconciliar, pintarTargeta i map pel mig, és fàcil acabar vint fotogrames dins de codi aliè. Per evitar-ho existeix Ignore List (abans blackboxing): marques un fitxer o un patró com a ignorat i el depurador no hi entra mai ni el mostra a la pila. Marca-hi les teves dependències i la pila es torna llegible a l'instant.
- Els panells Scope, Watch i Call Stack
Quan l'execució està aturada, la meitat dreta del panell és on passa la depuració de debò.
Scope mostra les variables agrupades per àmbit, i és la comprovació empírica de tot el que vas estudiar a 03-04 i 03-05:
Scope
├─ Local ← les variables de la funció actual
│ tasca: Tasca {id: 3, titol: 'Actualitzar el web…'}
│ nou: "en-curs"
├─ Closure (connectarTauler) ← el closure, visible!
│ tauler: Tauler {nom: 'Taller Nómada'}
│ avui: "2026-09-20"
├─ Module ← l'importat i el declarat al mòdul
└─ Global ← windowAquest bloc Closure és literalment l'entorn capturat del qual parlava 03-04, llistat amb nom i contingut. Si alguna vegada vas dubtar que els closures existeixen físicament, aquí els tens.
Watch és una llista d'expressions que es reavaluen a cada pas. No estàs limitat a variables: pots vigilar càlculs complets.
// Expressions útils a Watch mentre es depura Nómada Tasques
tauler.resum('2026-09-20').horesObertes
tauler.tasques.filter((t) => t.oberta).length
estat.filtres
document.querySelectorAll('#llista-tasques > li').lengthVeure horesObertes canviar de 45 a 39 al pas exacte en què passa és una informació impossible d'obtenir amb registres solts.
Call Stack és la pila de crides: qui ha cridat qui fins a arribar aquí. Es llegeix de dalt a baix, del més recent al més antic:
pintarTargeta targeta.js:24 ← ets aquí reconciliar dom.js:38 actualitzar tauler-vista.js:96 (anonymous) app.js:41 ← l'oient de l'esdeveniment
Prement qualsevol fotograma et trasllades a ell amb el seu Scope corresponent, sense perdre l'aturada. És la manera de respondre "amb quins arguments m'han cridat?" quan el problema no és aquí sinó a qui ha cridat.
I una funció poc coneguda i molt útil: Restart frame. Clic dret sobre un fotograma → Restart frame torna a executar aquella funció des del principi, amb els mateixos arguments. Si has passat de llarg la línia interessant amb un F10 de més, no cal recarregar la pàgina i repetir els deu clics que t'hi van portar: reinicies el fotograma i hi tornes a passar. (Amb un advertiment: els efectes secundaris que ja van passar —una petició enviada, un setItem fet— no es desfan.)
- Llegir un stack trace i «Pause on exceptions»
Un error sense capturar imprimeix una cosa així:
ErrorDeValidacio: Transició no permesa: "feta" → "en-curs".
at Tasca.canviarEstat (tasca.js:71:13)
at Tauler.canviarEstat (tauler.js:52:11)
at HTMLUListElement.<anonymous> (controlador.js:38:15)Es llegeix així:
- Primera línia: tipus d'error i missatge. Aquí ja saps que és una regla R6 incomplerta.
- Línia 1 de la pila: on es va llançar.
tasca.js:71:13= fitxer, línia 71, columna 13. - Línies següents: la cadena de crides cap enrere. L'última sol ser el punt d'entrada real.
HTMLUListElement.<anonymous>: una funció anònima adjuntada com a oient a un<ul>. És l'oient delegat de 06-04.
L'error més habitual en llegir una pila és fixar-se només en la primera línia. El lloc on es llança gairebé mai és el lloc on hi ha l'error. Aquí l'error es llança a Tasca, però la causa és a controlador.js:38: algú va intentar reobrir una tasca ja acabada perquè el botó no estava desactivat. La primera línia diu què ha passat; les de sota diuen per què.
«Pause on exceptions» (la icona ⏸ amb el rombe, al panell Sources) fa que el depurador s'aturi en l'instant en què es llança l'error, amb tot el context viu. Té dos nivells:
| Opció | S'atura a | Quan activar-la |
|---|---|---|
| Pause on uncaught exceptions | Només errors que ningú captura | Sempre; soroll gairebé nul |
| Pause on caught exceptions | També als que un catch captura |
Quan alguna cosa falla en silenci |
La segona és la joia amagada. Nómada Tasques captura errors en diversos llocs (el try/catch del formulari, el catch del repositori que descarta dades corrompudes, l'ambReintents). Si un error s'està empassant en un d'aquests catch, activar «pause on caught exceptions» et porta directament al throw original. Això sí: activa-la només mentre investigues, perquè també s'aturarà en errors capturats que són perfectament normals.
- Depurar codi asíncron
Aquí és on el depurador clàssic solia rendir-se. Quan un setTimeout, una promesa o un await reprenen l'execució, la pila de crides original ja s'ha buidat —és exactament el mecanisme del bucle d'esdeveniments que vas estudiar a 05-07—. Sense ajuda, la pila mostraria només això:
Inútil: no diu qui ha demanat aquella petició. Per això els navegadors moderns mantenen piles asíncrones, que cusen el fotograma actual amb el que va programar la tasca:
demanarJson http.js:112 ── Async: await ── ← la costura llistarTasques api-tasques.js:47 ── Async: await ── carregarTauler app.js:63 ── Async: promise callback ── (anonymous) controlador.js:52
Ara sí: la petició va néixer d'un clic al controlador. Quatre consells concrets per depurar asincronia en aquest projecte:
- Posa el punt d'interrupció després de l'
await, no abans. Abans només veus la promesa pendent; després veus el valor resolt. - Fes servir un punt condicional a
ambReintentsamb la condicióintent > 1: t'atures només quan alguna cosa ja ha fallat una vegada. - Al panell Network activa la columna Initiator. Igual que la pila asíncrona, et diu quina línia ha disparat cada petició.
- Compte amb les curses que el mateix depurador provoca. En aturar l'execució cinc segons, un timeout de 8 s pot vèncer. Si sospites d'una condició de cursa, prefereix logpoints a aturades.
Un patró molt útil per a l'asincronia és mesurar on se'n va el temps sense frenar res:
export async function llistarTasquesMesurat(filtres) {
console.time('llistarTasques');
try {
return await llistarTasques(filtres);
} finally {
console.timeEnd('llistarTasques'); // s'executa també si llança
}
}
// llistarTasques: 843.21 msEl finally garanteix que el timeEnd s'executi encara que la petició falli; si no, un error deixaria el temporitzador obert i el següent console.time avisaria d'una etiqueta duplicada.
- Source maps: per què producció és il·legible sense ells
El codi que es desplega no és el que escrius. Un empaquetador l'uneix, el minifica i reanomena les variables, així que un error a producció es veu així:
t, línia 1, columna 24817. Sense informació addicional, aquesta pista no val res.
Un source map és un fitxer (app.4f3a1b.js.map) que conté el diccionari de traducció entre el codi generat i l'original: quina posició del fitxer minificat correspon a quin fitxer, línia, columna i nom de variable del codi font. El navegador el carrega si troba el comentari final:
I aleshores el mateix error es mostra així:
TypeError: Cannot read properties of null (reading 'dataset')
at gestionarClic (js/vista/controlador.js:34:22)Tres decisions pràctiques sobre source maps:
| Escenari | Recomanació | Motiu |
|---|---|---|
| Desenvolupament | Sempre activats | El cost de mida és indiferent en local |
| Producció, aplicació pública | Generar-los, però no publicar-los a l'abast de qualsevol | Es pugen al servei de monitoratge; el navegador anònim no els descarrega |
| Producció, eina interna | Publicar-los sense problema | El codi no és secret i depurar incidències és més fàcil |
Tingues present que un source map reconstrueix el teu codi font. Publicar-lo equival a publicar el codi sense minificar. No és un error de seguretat per si mateix —la seguretat mai ha de dependre d'ofuscació—, però és una decisió conscient que cal prendre.
A DevTools, si un source map no carrega, la pestanya Sources mostra un avís a la consola (DevTools failed to load source map). Les tres causes habituals: el .map no es va desplegar, la ruta del comentari final és incorrecta, o el servidor el retorna amb un 404 després d'una configuració de memòria cau agressiva.
- Depurar la xarxa: el panell Network
El Mòdul 7 va omplir Nómada Tasques de peticions. Quan una falla, el panell Network respon en segons el que el codi no et pot explicar:
- Filtra per
Fetch/XHRper veure només les teves peticions, sense imatges ni CSS. - La columna Status distingeix el que
fetchno distingeix: un200amb cos buit, un404, un500, o(failed)per a una fallada de transport. - La pestanya Headers mostra el que vas enviar i el que va arribar, inclosa la capçalera
Content-Typeque fa fallar eldemanarJsonquan arriba HTML. - La pestanya Payload ensenya el cos del
POSTtal com va sortir: aquí es descobreixen elsundefinedqueJSON.stringifyelimina en silenci. - La pestanya Timing desglossa el temps: Stalled, Waiting (TTFB), Content Download. Si el TTFB és de 4 s, no és culpa del teu JavaScript.
Tres funcions del panell que convé conèixer:
Copy as fetch. Clic dret sobre una petició → Copy → Copy as fetch. Enganxa el resultat a la consola i reprodueixes la petició exacta, amb les seves capçaleres, tantes vegades com vulguis. És la millor manera d'aïllar si el problema és al servidor o al teu codi: si la petició copiada funciona a la consola i no a l'aplicació, l'error és teu.
// Enganxat des de "Copy as fetch" i modificat per provar una hipòtesi
await fetch('https://api.tallernomada.example/v1/tasques?responsable=Iv%C3%A1n', {
headers: { accept: 'application/json' }
}).then((r) => ({ ok: r.ok, status: r.status, tipus: r.headers.get('content-type') }));Throttling. El selector de xarxa (No throttling / Slow 4G / Offline) simula connexions lentes. És imprescindible per provar dues coses que vas escriure al Mòdul 7 i que en local no es veuen mai: els estats de "carregant" de 07-03 i el funcionament sense connexió del service worker de 07-05. En una xarxa local d'1 ms, l'estat de càrrega apareix i desapareix abans de renderitzar-se.
Preserve log. Marca aquesta casella si la petició que investigues provoca una navegació o una recàrrega; sense ella, el registre s'esborra i la petició culpable desapareix.
- Depurar l'emmagatzematge: el panell Application
Per a tot el de 07-01 i 07-05, el panell Application és la finestra directa a l'estat persistit:
| Secció | Què inspecciones a Nómada Tasques |
|---|---|
| Local Storage | La clau nomada:tauler:v1, el seu JSON i la seva mida |
| Session Storage | Estat efímer de la pestanya |
| IndexedDB | (No fet servir aquí, però és on miraria una aplicació més gran) |
| Cache Storage | Els recursos precarregats a la memòria cau pel service worker |
| Service Workers | Estat del worker: instal·lat, actiu, en espera; Update on reload, Unregister |
| Manifest | Com interpreta el navegador el teu manifest.json |
El valor de localStorage es pot editar allà mateix: fas doble clic al valor, canvies el JSON i recarregues. És la manera més ràpida de comprovar com reacciona el teu codi a dades corrompudes, a un format de versió antiga, o a un tauler buit… sense escriure ni una línia. I Clear site data et torna a un arrencada neta, que és l'estat en què has de reproduir qualsevol error abans de donar-lo per bo.
- Depurar en un mòbil real
L'emulador de dispositiu de les DevTools canvia la mida i simula el tàctil, però no és Safari en un iPhone ni Chrome en un Android de gamma baixa. Els errors de debò —un 100vh que es menja la barra d'adreces, un esdeveniment tàctil que no fa bombolla, una API que no existeix en aquella versió— només es veuen a l'aparell.
La inspecció remota connecta les DevTools del teu ordinador a la pàgina que s'executa al telèfon:
- Android + Chrome: activa les Opcions de desenvolupador i la Depuració per USB al telèfon, connecta'l per cable i obre
chrome://inspect#devicesa l'ordinador. La pestanya del telèfon apareix a la llista; prems inspect i tens les DevTools completes, inclosos el depurador i el panell Network. - iOS + Safari: activa Configuració → Safari → Avançat → Inspector web a l'iPhone i Safari → Configuració → Avançat → Mostra el menú Desenvolupament al Mac; el dispositiu apareix sota el menú Desenvolupament.
I per provar Nómada Tasques al mòbil cal que el telèfon arribi al teu servidor de desenvolupament. Dues vies:
# Opció A · mateixa xarxa wifi: serveix a totes les interfícies i fes servir la IP local
npx serve -l tcp://0.0.0.0:5000
# El mòbil obre http://192.168.1.42:5000
# Opció B · túnel públic amb HTTPS (necessari per a service workers reals)
npx localtunnel --port 5000L'opció B importa per un detall de 07-05: els service workers exigeixen HTTPS excepte a localhost. I localhost al mòbil és el mateix mòbil, no el teu portàtil. Sense túnel HTTPS no podràs depurar la PWA en un dispositiu real.
- Cas 1: la tasca que no canvia d'estat
Apliquem el mètode complet a un error real.
El part d'incidència. La Marta escriu: «He creat la tasca Revisar els extintors amb el formulari i després he premut Començar i no fa res. Les altres sí que funcionen.»
Pas 1 · Reproduir. Primer, una arrencada neta (Clear site data). Després, la seqüència exacta:
- Obrir l'aplicació amb el backlog canònic (6 tasques).
- Prémer Començar a la tasca 2 → funciona.
- Crear una tasca nova amb el formulari.
- Prémer Començar a la tasca nova → no passa res.
Reproduït, i amb una pista d'or: falla només a les tasques creades durant la sessió. Les del backlog van bé.
Pas 2 · Aïllar. És del formulari o de la creació? Provem de crear una tasca des de la consola, sense tocar el formulari:
const t = await crearTasca({ titol: 'Prova', responsable: 'Iván', prioritat: 'baixa',
horesEstimades: 2, dataLimit: '2026-10-30', etiquetes: [] });
tauler.afegir(t);
vista.actualitzar();
// Prémer "Començar" en aquella targeta → tampoc funcionaEl formulari queda descartat: el problema és a les tasques que vénen de crearTasca, és a dir, de l'API.
Pas 3 · Hipòtesi. El flux del clic és: controlador llegeix li.dataset.id → crida tauler.canviarEstat(id, ...) → cercarPerId(id) compara amb t.id === id. I dataset sempre retorna cadenes. Per a les tasques del backlog, id és un número al model; per a les de l'API… la hipòtesi concreta és:
Tasca.desDeJSONestà desantidcom a cadena per a les tasques que arriben del servidor, icercarPerIdfa servir===, que no converteix tipus (01-07). Per aixòcercarPerId('7')retornanulli no troba la tasca.
Espera: si retornés null, Tauler.canviarEstat llançaria ErrorDeValidacio. Per què no veiem res? Perquè el controlador captura aquest error per mostrar-lo, i el catch l'escriu en un contenidor que està ocult. Segona part de la hipòtesi: l'error es llança i s'empassa.
Pas 4 · Comprovar. Dues comprovacions, cap no modifica el codi:
- Activar Pause on caught exceptions, prémer el botó. El depurador s'atura… al
throwdeTauler.canviarEstat. Hipòtesi B confirmada. - Posar un logpoint a la línia de
cercarPerIdamb{id, tipusId: typeof id, ids: this.tasques.map((t) => [t.id, typeof t.id])}:
Confirmadíssim. L'id del model és una cadena '7', el dataset també retorna cadena, però el controlador feia Number(li.dataset.id) abans de cridar, així que compara 7 === '7' → false.
Pas 5 · Corregir la causa. Tres arranjaments possibles, i només un és el correcte:
| Arranjament | On | Veredicte |
|---|---|---|
Canviar === per == a cercarPerId |
Model | ❌ Tapa el símptoma i reintrodueix la coerció que 01-07 desaconsella |
| Convertir a número al controlador | Vista | ❌ El model continuaria amb tipus barrejats; l'error reapareixeria en un altre lloc |
| Normalitzar el tipus a la frontera de dades | Tasca.desDeJSON |
✅ Un sol punt, i l'invariant «id és number» es compleix sempre |
// js/model/tasca.js — la frontera normalitza els tipus
static desDeJSON(dades) {
const pla = typeof dades === 'string' ? JSON.parse(dades) : dades;
return new Tasca({ ...pla, id: Number(pla.id) }); // ← l'id SEMPRE és número
}I de passada, el catch que s'empassava l'error deixa de ser silenciós: si el contenidor d'errors està ocult, es mostra. Un error que ningú veu és un error que existeix dues vegades.
Pas 6 · Prevenir. La prova que hauria d'existir, i que escriuràs a 08-03:
// Pendent per a 08-03:
// "Tasca.desDeJSON converteix un id en cadena a número"
// → Tasca.desDeJSON({ ...dades, id: '7' }).id === 7 (i typeof === 'number')
// "Tauler.cercarPerId troba una tasca importada del servidor"
- Cas 2: el filtre que perd tasques en recarregar
El part. L'Iván: «He filtrat pel meu nom per veure les meves tasques, he tancat el portàtil, i en tornar només quedaven tres tasques a tot el tauler. Les de la Marta i la Lucía han desaparegut.»
Pas 1 · Reproduir. Amb el backlog canònic: filtrar per Iván (queden 3 visibles), recarregar (F5) → el tauler té 3 tasques i el resum diu 25 h. Reproduïble al 100 %. I és un error de pèrdua de dades, la pitjor categoria: prioritat màxima.
Pas 2 · Aïllar. La pregunta clau: s'ha desat malament, o es llegeix malament? El panell Application la respon sense tocar el codi. Filtrem per Iván (sense recarregar) i mirem Local Storage → nomada:tauler:v1:
{ "nom": "Taller Nómada", "versio": 1, "tasques": [
{ "id": 1, "titol": "Redissenyar la sala polivalent", "responsable": "Iván", … },
{ "id": 5, "titol": "Guia sobre enquadernació per a residents", … },
{ "id": 6, "titol": "Pressupost de la fusteria", … } ] }Només tres tasques ja escrites. L'error és a l'escriptura, no a la lectura. Acabem de descartar la meitat del recorregut amb una ullada.
Pas 3 · Hipòtesi. Qui crida desar i amb què? Aquí l'eina és console.trace() col·locat com a logpoint a RepositoriLocal.desar, amb l'expressió:
desar {total: 3}
console.trace
at HTMLDocument.<anonymous> (app.js:78)
at TaulerVista.actualitzar (tauler-vista.js:104)La hipòtesi s'escriu sola:
app.jsestà desant el que la vista mostra en lloc del tauler complet. En filtrar, la vista té 3 tasques i això és el que es persisteix, esclafant les altres 3.
Pas 4 · Comprovar. Obrim app.js:78:
document.addEventListener(ESDEVENIMENTS.FILTRE_APLICAT, () => {
vista.actualitzar({ filtres: llegirEstatDeUrl() });
repositori.desar(new Tauler(tauler.nom, vista.visibles)); // ← aquí
});Algú, en afegir l'encaminador de 07-06, va voler "desar l'estat en filtrar" i va construir un tauler nou amb les tasques visibles. Hipòtesi confirmada amb la línia al davant.
Pas 5 · Corregir. La causa real és conceptual, i mereix enunciar-se: s'ha persistit un valor derivat de la vista com si fos estat del model. El filtre és presentació (06-06) i el seu lloc és la URL (07-06); el tauler és el model i el seu lloc és el magatzem.
// js/app.js — corregit
document.addEventListener(ESDEVENIMENTS.FILTRE_APLICAT, (esdeveniment) => {
vista.actualitzar({ filtres: esdeveniment.detail });
escriureEstatAUrl(esdeveniment.detail); // el filtre viu a la URL…
});
// …i el model es desa només quan canvia el MODEL
document.addEventListener(ESDEVENIMENTS.TASCA_CANVIADA, () => repositori.desar(tauler));
document.addEventListener(ESDEVENIMENTS.TASCA_CREADA, () => repositori.desar(tauler));Pas 6 · Prevenir. Dues comprovacions per a 08-05, on es proven model i dades junts:
// Pendent per a 08-05:
// "filtrar la vista no altera el que està persistit"
// → filtrar per 'Iván', i repositori.carregar().total continua sent 6
// "desar després de canviar d'estat conserva les 6 tasques"
- Cas 3: el comptador d'hores descompensat
El part. La Lucía: «El resum diu 48 h obertes tot just obrir. Haurien de ser 45; la tasca de l'inventari està feta des de fa una setmana.»
Aquest cas és diferent dels anteriors: no cal reproduir res, falla sempre. I hi ha una cosa molt millor que un part d'incidència: hi ha un oracle. El backlog canònic té números coneguts —48 h totals, 45 h obertes, esforç 124— i el programa en contradiu un.
Pas 2 · Aïllar. Una sola ordre a la consola separa el model de la vista:
tauler.resum('2026-09-20');
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 48, vencudes: 1, esforc: 124 }El model ja retorna 48. La vista és innocent: només pinta el que li donen. I hi ha un detall revelador: obertes: 5 és correcte (5 tasques obertes de 6), però horesObertes coincideix exactament amb horesTotals. Això no és un error de càlcul, és un error de selecció: s'estan sumant les sis.
Pas 3 · Hipòtesi.
horesObertesestà reduint sobre totes les tasques en lloc de sobre les obertes.
Pas 4 · Comprovar. Posem el punt d'interrupció al getter i fem servir Watch amb dues expressions alhora:
// A Watch:
this.obertes.length // → 5
this.obertes.reduce((s, t) => s + t.horesEstimades, 0) // → 45 ← el valor correcteL'expressió correcta dóna 45. Mirem el codi:
get horesTotals() { return this.#tasques.reduce((s, t) => s + t.horesEstimades, 0); }
get horesObertes() { return this.#tasques.reduce((s, t) => s + t.horesEstimades, 0); }
// ^^^^^^^^^^^^ hauria de ser this.obertesUn copiar i enganxar. L'error clàssic: dues línies gairebé idèntiques, una d'elles sense adaptar. Sospita sempre de les línies bessones.
Comprovació addicional amb git, per saber quan va entrar i descartar que hi hagués més danys al mateix canvi:
git log -L :horesObertes:js/model/tauler.js
# mostra la història completa d'AQUESTA funció, amb el commit que la va trencarPas 5 · Corregir.
Pas 6 · Prevenir. Aquest cas és l'argument perfecte per al mòdul sencer. Un error així:
- No llança cap error.
- No trenca cap pantalla.
- És invisible llevat que algú conegui el número correcte.
- I surt d'una capa —el model— que es pot comprovar sense navegador, sense DOM i sense xarxa, amb una funció que rep dades i retorna dades.
// Pendent per a 08-03 (serà literalment la primera prova que escriuràs):
// "el resum del backlog canònic dóna 45 h obertes de 48 i esforç 124"Mentrestant, una xarxa de seguretat de trenta segons que ja pots deixar posada, activada per una bandera:
// js/app.js — comprovació d'invariants, només en desenvolupament
if (localStorage.getItem('nomada:diag') === '1') {
const r = tauler.resum(AVUI);
console.assert(r.horesTotals === r.horesObertes + 3, 'horesObertes descompensat', r);
console.assert(r.total === r.obertes + 1, 'recompte d obertes descompensat', r);
}
- El bug que no es reprodueix
Queda la categoria més difícil: «de vegades, en sortir de l'ascensor, se'm dupliquen les tasques». No hi ha passos, no hi ha pantalla, no hi ha pila. Quatre eines, en ordre d'esforç creixent:
1 · Registre estructurat. Substitueix els console.log solts per un registrador únic que emeti objectes amb context. Els objectes es poden filtrar, comptar i enviar; el text lliure no.
// js/util/registre.js
const NIVELLS = { debug: 10, info: 20, warn: 30, error: 40 };
let llindar = NIVELLS.warn; // en producció, només warn i error
export function ajustarNivell(nom) { llindar = NIVELLS[nom] ?? NIVELLS.warn; }
export function registrar(nivell, esdeveniment, dades = {}) {
if (NIVELLS[nivell] < llindar) return;
const linia = {
ts: new Date().toISOString(),
nivell,
esdeveniment, // 'tasca:canviada', 'api:error', 'sw:activat'
sessio: sessioId(), // el mateix id durant tota la visita
...dades
};
console[nivell === 'debug' ? 'log' : nivell](linia);
historial.push(linia); // buffer circular en memòria
if (historial.length > 200) historial.shift();
}
const historial = [];
export const abocarHistorial = () => [...historial]; // per adjuntar a un informeEl buffer circular és la clau: quan l'error rar per fi passa, tens les 200 línies anteriors, no només el moment del desastre. Aquest és exactament el context que falta als incidents que no es reprodueixen.
2 · Banderes de diagnòstic. No pots demanar a la Marta que obri les DevTools. Però sí que li pots donar un interruptor:
// Activable amb ?diag=1 a la URL, i persistent fins que es desactivi
const params = new URLSearchParams(location.search);
if (params.has('diag')) localStorage.setItem('nomada:diag', params.get('diag'));
if (localStorage.getItem('nomada:diag') === '1') ajustarNivell('debug');I un botó «Copiar informe de diagnòstic» que posi al porta-retalls l'historial, el resum del tauler, la versió de l'aplicació i el navigator.userAgent. Un informe així converteix «de vegades falla» en un cas reproduïble.
3 · Reproduir les condicions, no els passos. Els errors intermitents gairebé sempre vénen de quatre llocs: la xarxa (fes servir Slow 4G i Offline), el temps (una tasca que venç avui i no ahir), la concurrència (dues pestanyes obertes amb l'esdeveniment storage de 07-01) i l'estat previ (un localStorage d'una versió antiga). Provoca aquestes condicions deliberadament i molts "irreproduïbles" es tornen fiables.
4 · Monitoratge d'errors. Per al que passa a l'ordinador d'una altra persona, la solució de la indústria és un servei de monitoratge (Sentry, Rollbar, Bugsnag i similars). El mecanisme essencial cap en deu línies, i entendre'l importa més que el proveïdor concret:
// Captura global: errors síncrons i promeses rebutjades sense gestionar
window.addEventListener('error', (e) => {
enviarIncidencia({ tipus: 'error', missatge: e.message, fitxer: e.filename,
linia: e.lineno, pila: e.error?.stack });
});
window.addEventListener('unhandledrejection', (e) => { // ← el de 05-06
enviarIncidencia({ tipus: 'promesa', missatge: String(e.reason), pila: e.reason?.stack });
});El que un servei afegeix sobre això: agrupa incidències idèntiques, aplica els source maps de l'apartat 12 per mostrar el teu codi original, desa la traça d'accions prèvies de l'usuari, i avisa quan apareix un error nou després d'un desplegament. Dos advertiments imprescindibles: no enviïs dades personals al context (ni títols de tasca, si poden contenir informació sensible), i respecta la normativa de protecció de dades aplicable.
Errors Habituals i Consells
- Canviar el codi abans d'entendre l'error. Si no pots explicar per què falla, el teu arranjament és una aposta. Primer la hipòtesi comprovada, després l'edició.
- Depurar en un estat brut. Un
localStorageamb dades de tres experiments anteriors produeix errors fantasma. Reprodueix sempre des deClear site data, i en una finestra d'incògnit si sospites d'una extensió. - Confondre on es llança l'error amb on hi ha l'error. Llegeix la pila sencera, de baix a dalt. El culpable sol estar dos fotogrames més avall que el
throw. - Oblidar que
datasetretorna cadenes. És la font del cas 1 i de la meitat dels errors d'identitat en aplicacions amb DOM. Normalitza els tipus a la frontera, sempre. console.logd'un objecte i creure que en veus el valor d'aleshores. La consola mostra objectes en viu: en desplegar-lo pots estar veient el seu estat actual, no el del moment del registre. Si necessites la foto, registrastructuredClone(obj)(04-08) oJSON.stringify(obj).- Posar el punt d'interrupció abans de l'
await. Posa'l després: abans només veuràs una promesa pendent. - Deixar
debuggero registres de depuració en un commit. Es resol tot sol a la lliçó següent, ambno-debuggerino-consolea ESLint més un hook de Git. - Tapar errors amb
try { … } catch {}buit. Uncatchbuit converteix un error sorollós en un de silenciós, que és infinitament pitjor. Si de veritat vols ignorar alguna cosa, escriu per què en un comentari. - Consell: escriu la hipòtesi en una nota abans de comprovar-la. Obliga a concretar i evita la deriva d'anar mirant coses a l'atzar. En acabar tindràs a més el material del post mortem.
- Consell: si portes més d'una hora sense avançar, explica-ho a algú. El rubber duck debugging funciona perquè verbalitzar obliga a fer explícites les suposicions. La meitat de les vegades la solució apareix a mitja explicació.
- Consell:
Ctrl/Cmd + Pal panell Sources obre qualsevol fitxer pel nom, iCtrl/Cmd + Maj + Pés la paleta d'ordres de les DevTools. Amb aquestes dues dreceres deixaràs de buscar fitxers a l'arbre.
Exercicis
Exercici 1 — Diagnòstic amb el mètode.
Un company reporta: «En prémer dues vegades ràpid sobre Començar a la tasca 1, la targeta es queda en en-curs però el resum diu que hi ha 4 tasques obertes en lloc de 5, i a la consola apareix un ErrorDeValidacio: Transició no permesa: "en-curs" → "en-curs"». Redacta el diagnòstic complet seguint els sis passos: passos de reproducció, cas mínim aïllat, hipòtesi falsable, quin instrument de DevTools faries servir per comprovar-la (indica el tipus exacte de punt d'interrupció i la seva condició), en quina capa corregiries, i quina prova escriuries per prevenir-ho.
Exercici 2 — Un mòdul de registre amb nivells i buffer.
Escriu js/util/registre.js complet amb: quatre nivells (debug, info, warn, error), un llindar configurable per bandera (?diag=1 a la URL, persistit a localStorage), un identificador de sessió estable durant tota la visita, un buffer circular de les últimes 200 línies, i abocarInforme() que retorni un objecte amb l'historial, el resum del tauler, la versió de l'aplicació i el userAgent, llest per copiar al porta-retalls. Ha de sortir per console.debug/info/warn/error segons el nivell perquè el filtre de la consola funcioni.
Exercici 3 — Instrumentar el flux d'un clic.
Sense modificar el codi de l'aplicació, descriu la configuració de DevTools que et permetria seguir el recorregut complet d'un clic a Començar de la tasca 3 —des de l'oient delegat fins a l'escriptura a localStorage— registrant a cada punt els valors rellevants i sense aturar mai l'execució. Indica el fitxer, la línia aproximada i l'expressió exacta de cada logpoint, i què esperaries veure a la consola si tot funciona bé.
Solucions
Solució 1
1 · REPRODUIR
- Clear site data. Carregar amb el backlog canònic.
- Doble clic ràpid (< 100 ms) sobre "Començar" de la tasca 1.
- Es reprodueix 3 de cada 3 vegades si el segon clic arriba abans del render.
2 · AÏLLAR
- Cas mínim: dues crides seguides a controlador.avancar(1) sense render intermedi.
tauler.canviarEstat(1, 'en-curs'); tauler.canviarEstat(1, 'en-curs');
- Sense DOM: el segon llança el mateix ErrorDeValidacio. L'error NO és del navegador
ni del doble clic: és que l'estat següent es calcula des del DOM, no des del model.
3 · HIPÒTESI (falsable)
El controlador calcula l'estat destí a partir de `li.dataset.estat`, que només
s'actualitza al render. Entre el primer clic i el seu render, el dataset encara diu
'pendent', així que el segon clic torna a demanar 'en-curs' → transició invàlida (R6).
I el resum es recalcula al catch amb un comptador incrementat a mà, que ja s'havia
sumat abans de llançar: d'aquí el 4 en lloc del 5.
4 · COMPROVAR
- Punt d'interrupció CONDICIONAL a controlador.js, línia del càlcul del destí:
condició: id === 1
- I un logpoint a la mateixa línia:
'avancar', {id, datasetEstat: li.dataset.estat, modelEstat: tauler.cercarPerId(id).estat}
Si la hipòtesi és certa, al segon clic es veurà:
{id: 1, datasetEstat: 'pendent', modelEstat: 'en-curs'} ← divergeixen
- Activar "Pause on caught exceptions" per confirmar que l'error s'empassa al catch.
5 · CORREGIR (capa: vista/controlador)
- L'estat destí es calcula SEMPRE des del model, mai des del DOM:
const tasca = tauler.cercarPerId(id);
const desti = SEGUENT[tasca.estat];
if (desti === null) return;
- El DOM és una projecció de l'estat, no la seva font (cicle de 06-06).
- A més, el resum es recalcula amb tauler.resum(AVUI), sense comptadors manuals.
6 · PREVENIR (per a 08-03)
- "dos canviarEstat consecutius al mateix destí llancen ErrorDeValidacio" (model).
- "dos clics seguits a avançar deixen la tasca en 'en-curs' i el resum en 5 obertes"
(integració amb jsdom, 08-05).Solució 2
// js/util/registre.js
const NIVELLS = Object.freeze({ debug: 10, info: 20, warn: 30, error: 40 });
const SORTIDA = Object.freeze({ debug: 'debug', info: 'info', warn: 'warn', error: 'error' });
const MAXIM = 200;
const historial = [];
let llindar = NIVELLS.warn;
/** Bandera de diagnòstic: ?diag=1 l'activa i queda persistida fins a ?diag=0. */
function llegirBandera() {
const p = new URLSearchParams(location.search);
if (p.has('diag')) localStorage.setItem('nomada:diag', p.get('diag'));
return localStorage.getItem('nomada:diag') === '1';
}
/** Un id per visita: permet agrupar totes les línies d'una mateixa sessió. */
function sessioId() {
let id = sessionStorage.getItem('nomada:sessio');
if (id === null) {
id = crypto.randomUUID();
sessionStorage.setItem('nomada:sessio', id);
}
return id;
}
export function ajustarNivell(nom) {
llindar = NIVELLS[nom] ?? NIVELLS.warn;
}
export function registrar(nivell, esdeveniment, dades = {}) {
const linia = { ts: new Date().toISOString(), nivell, esdeveniment, sessio: sessioId(), ...dades };
historial.push(linia); // el buffer desa TOT, encara que no s'imprimeixi
if (historial.length > MAXIM) historial.shift();
if (NIVELLS[nivell] >= llindar) console[SORTIDA[nivell]](`[nomada] ${esdeveniment}`, linia);
return linia;
}
export const debug = (esdeveniment, dades) => registrar('debug', esdeveniment, dades);
export const info = (esdeveniment, dades) => registrar('info', esdeveniment, dades);
export const avis = (esdeveniment, dades) => registrar('warn', esdeveniment, dades);
export const fallada = (esdeveniment, dades) => registrar('error', esdeveniment, dades);
/** Informe complet, llest per copiar i adjuntar a una incidència. */
export function abocarInforme({ tauler, avui, versio = '1.0.0' } = {}) {
return {
versio,
generat: new Date().toISOString(),
sessio: sessioId(),
userAgent: navigator.userAgent,
diagnostic: llegirBandera(),
resum: tauler ? tauler.resum(avui) : null,
historial: structuredClone(historial) // còpia profunda: la foto, no l'objecte viu
};
}
export async function copiarInforme(context) {
await navigator.clipboard.writeText(JSON.stringify(abocarInforme(context), null, 2));
}
// Arrencada
if (llegirBandera()) ajustarNivell('debug');Solució 3
Configuració de DevTools (cap línia de codi modificada):
A · Ignore List
Afegir qualsevol dependència externa perquè "step into" no es perdi.
B · Quatre logpoints (clic dret al número de línia → Add logpoint)
1) js/vista/controlador.js — dins de l'oient delegat, després del closest():
'A·clic', {accio: boto?.dataset.accio, id: boto?.closest('[data-id]')?.dataset.id}
→ esperat: {accio: 'avancar', id: '3'} (cadena! compte amb el cas 1)
2) js/model/tauler.js — primera línia de canviarEstat(id, nou):
'B·model', {id, tipus: typeof id, nou, actual: this.cercarPerId(id)?.estat}
→ esperat: {id: 3, tipus: 'number', nou: 'en-curs', actual: 'pendent'}
3) js/vista/tauler-vista.js — primera línia d'actualitzar():
'C·render', {visibles: this.visibles?.length, resum: this.estat?.tauler.resum('2026-09-20')}
→ esperat: {visibles: 6, resum: {…, horesObertes: 45, …}}
4) js/dades/repositori-local.js — primera línia de desar(tauler):
'D·persistir', {total: tauler.total, bytes: JSON.stringify(tauler).length}
→ esperat: {total: 6, bytes: ~1200}
C · Comprovació creuada
- console.count activat al logpoint C detectaria renders duplicats.
- Si apareixen A i B però no C, l'esdeveniment 'tasca:canviada' no s'està emetent.
- Si apareixen A, B i C però no D, falta l'oient de persistència (cas 2).
- Si D diu total: 3 mentre C diu visibles: 3, s'està persistint la vista (cas 2).
D · Extra sense aturar res
Un breakpoint per canvi del DOM (attribute modifications) sobre el <li data-id="3">
assenyalaria exactament quina línia escriu data-estat, útil si C s'executa però la
targeta no canvia.Conclusió
Has canviat la manera d'enfrontar-te a un error. Ja no es tracta de mirar el codi fins que alguna cosa sembli sospitosa, sinó de recórrer sis passos: reproduir de manera fiable, aïllar fins al cas mínim, formular una hipòtesi falsable, comprovar-la amb un instrument, corregir la causa a la capa correcta i prevenir amb una prova. I saps que quan el terreny és gran, la bisecció —al flux de dades, a la història amb git bisect o al mateix codi— converteix una cerca lineal en una de logarítmica.
Coneixes la consola sencera i no només console.log: table per veure sis tasques d'una ullada, count per descobrir els renders duplicats, assert per vigilar invariants com les 45 h obertes, group per no ofegar-te en línies, trace per saber qui ha cridat, i el truc de les claus console.log({variable}) que etiqueta cada valor amb el seu nom. I domines el depurador de debò: la sentència debugger i els seus riscos, els sis tipus de punt d'interrupció —normal, condicional, de registre, per esdeveniment, per petició i per canvi del DOM—, la navegació amb step over, into i out, els panells Scope (on els closures de 03-04 són per fi visibles), Watch amb expressions calculades i Call Stack amb el seu Restart frame. Saps llegir un stack trace de baix a dalt, activar «Pause on caught exceptions» per caçar els errors que s'empassen els catch, seguir una pila asíncrona cosida a través dels await de 05-07, i per què sense source maps un error de producció no diu res. I tens el panell Network amb el seu Copy as fetch i el seu throttling, el panell Application per al localStorage i el service worker del Mòdul 7, i la inspecció remota per al mòbil de debò.
Sobretot, has resolt tres errors reals amb el mètode al davant: un id que arribava com a cadena des de l'API i feia fallar el === de 01-07 —corregit a la frontera de dades, no al model ni a la vista—; un filtre que persistia les tasques visibles en lloc del tauler complet, confonent un valor derivat de la vista amb l'estat del model; i un horesObertes que sumava sobre totes les tasques i retornava 48 en lloc de 45, un error silenciós que cap pantalla delatava. I has vist què fer quan l'error no es reprodueix: registre estructurat amb buffer circular, banderes de diagnòstic activables per URL, reproducció de condicions en lloc de passos, i una nota sobre els serveis de monitoratge que apliquen els teus source maps als errors d'altres persones.
Els tres casos comparteixen un final incòmode: tots tres s'han tancat amb una prova «pendent d'escriure». I tots tres, a més, tenien un avís previ que ningú va veure. L'id barrejant tipus, el catch buit que s'empassava l'error, les dues línies bessones on una no es va adaptar: són exactament el tipus de problema que una màquina pot assenyalar abans d'executar res, llegint el codi. Abans d'automatitzar les comprovacions de comportament convé automatitzar les de forma, perquè són més barates i atrapen una família sencera d'errors elles soles. Això és el següent: Qualitat de Codi: ESLint, Prettier i Convencions, on configuraràs un analitzador estàtic sobre Nómada Tasques, arreglaràs els avisos que apareguin —inclòs, amb tota seguretat, algun debugger oblidat d'aquesta lliçó— i posaràs un guardià a Git perquè res d'això no torni a arribar al repositori.
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
