Les proves et diuen que alguna cosa està trencada. Gairebé mai no et diuen per què. Una prova d'integració que esperava 201 i ha rebut 500 t'ha fet un favor enorme —ha detectat la fallada abans que un client—, però ara cal esbrinar què ha passat dins d'aquella petició: en quin middleware, amb quines dades, en quina línia.
Aquesta lliçó tanca el mòdul amb l'altra meitat de l'ofici: les eines i el mètode per diagnosticar. Una frontera abans de començar, perquè no hi hagi confusió: aquí diagnostiquem el que falla. Mesurar i optimitzar el que funciona però va lent —perfils de CPU, flamegraphs, proves de càrrega— és el mòdul 10.
Contingut
- Del
console.logal depurador - El depurador integrat:
--inspect - Depurar des de VS Code, incloses les proves de Mocha
- Punts d'interrupció i execució pas a pas
- Depurar codi asíncron
- Llegir una traça d'error de veritat
- Errors freqüents i el seu diagnòstic
- Fuites de memòria
- Depurar en producció sense aturar el servei
- Mètode de depuració
Del console.log al depurador
Comencem per l'obvi: console.log no és cap vergonya. És instantani, funciona en qualsevol entorn, no requereix configuració i moltes vegades resol el problema en trenta segons. El que sí que és cert és que obliga a modificar el codi, a reiniciar, a endevinar per endavant què imprimir, i amb objectes imbricats imprimeix [Object] justament on hi havia la resposta. Aprofita primer tot el que la consola ofereix més enllà de log:
const util = require('node:util');
console.table(esdeveniment.sessions.map((s) => ({ // comparatives llegibles d'un cop d'ull
id: s.id, aforament: s.aforament, lliures: s.lliures,
ocupacio: `${(s.ocupacio * 100).toFixed(1)} %`,
})));
console.dir(comanda, { depth: null, colors: true }); // el remei contra [Object]
registre.debug(util.inspect(comanda, { depth: 4, breakLength: 120 })); // igual, com a text
console.time('compra'); // quant triga un tram
await comprarEntrades(dades);
console.timeEnd('compra'); // compra: 187.42ms
console.trace('qui esta cridant vendre()'); // com s'ha arribat aqui, sense llancar
console.count('venda-registrada'); // quantes vegades passa per aqui
console.assert(sessio.venudes <= sessio.aforament, 'SOBREVENDA', sessio.id);console.dir(objecte, { depth: null }) és probablement el truc més rendible de la llista: per defecte Node només imprimeix dos nivells d'imbricació, i amb una comanda que conté línies que contenen entrades aquests dos nivells s'exhaureixen de seguida.
Tot i així, el depurador dona tres coses que la consola no pot donar:
console.log |
Depurador | |
|---|---|---|
| Decidir què mirar | Abans d'executar | Durant l'execució |
| Veure l'àmbit | Només el que has imprès | Totes les variables vives |
| Pila de crides | console.trace |
Navegable, amb l'àmbit de cada marc |
| Modificar valors | No | Sí, des de la consola en viu |
| Cost en producció | Contamina els registres | Zero si no està actiu |
El depurador integrat: --inspect
Node porta un depurador incorporat que parla el protocol de Chrome DevTools, i s'activa amb dos indicadors:
node --inspect src/servidor.js # arrenca i activa l'inspector
node --inspect-brk src/servidor.js # arrenca i S'ATURA a la primera linia
# Debugger listening on ws://127.0.0.1:9229/8f2a1c3d-...La diferència és decisiva. --inspect serveix per depurar una cosa que passa més tard —una petició HTTP concreta, un esdeveniment—: el servidor arrenca i espera. --inspect-brk serveix per depurar l'arrencada: la lectura de la configuració, la connexió a la base de dades, el muntatge dels middlewares; sense -brk, tot això ja ha passat abans que tinguis temps de connectar-t'hi.
Per connectar-t'hi des de Chrome o Edge, obre chrome://inspect, busca el teu procés a «Remote Target» i prem inspect: s'obre DevTools amb la pestanya Sources, on pots posar punts d'interrupció, i amb la pestanya Memory, que farem servir per a les fuites. Existeix també un depurador de línia d'ordres, node inspect src/servidor.js, útil per SSH on no hi ha navegador: c continua, n avança una línia, s entra a la funció, o en surt, repl avalua expressions. És auster, però un dia en un servidor remot et salvarà la tarda.
Depurar des de VS Code, incloses les proves de Mocha
Aquesta és la manera més productiva en el dia a dia. A .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Servidor Escena Viva",
"type": "node", "request": "launch",
"program": "${workspaceFolder}/src/servidor.js",
"envFile": "${workspaceFolder}/.env",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
"console": "integratedTerminal"
},
{
"name": "Mocha: el fitxer obert",
"type": "node", "request": "launch",
"program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
"args": ["${relativeFile}", "--timeout", "0"],
"envFile": "${workspaceFolder}/.env.test",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
"console": "integratedTerminal"
},
{
"name": "Adjuntar a un proces existent",
"type": "node", "request": "attach", "port": 9229, "restart": true,
"skipFiles": ["<node_internals>/**"]
}
]
}La segona configuració —obres comandes.test.js, hi poses un punt d'interrupció i prems F5— és la més usada de totes; n'hi ha prou de duplicar-la sense ${relativeFile} per depurar la bateria completa. La tercera serveix quan el procés ja s'executa amb --inspect, per exemple dins d'un contenidor.
Tres detalls marquen la diferència. "--timeout", "0" és imprescindible a les configuracions de Mocha: sense ell, Mocha avorta la prova als 5000 ms mentre tu estàs aturat llegint variables. skipFiles amb node_modules i <node_internals> evita que el pas a pas et fiqui dins d'Express o de Mongoose, de manera que en «entrar a la funció» entris al teu codi. I envFile apuntant a .env.test garanteix que depures contra la base de proves i no contra la de desenvolupament.
Depurar les proves és, de bon tros, el cas més útil: tens una fallada reproduïble de manera determinista, aïllada, amb dades conegudes, i la pots aturar on vulguis. És la situació ideal per depurar, i és un regal que t'ha fet la resta del mòdul.
Punts d'interrupció i execució pas a pas
Hi ha tres tipus de punt d'interrupció, i només se'n fa servir el primer. El normal atura sempre en aquella línia. El condicional atura només si es compleix una expressió, i és l'eina que converteix una tarda en cinc minuts: amb la condició sessio.id === 'ses-001-1' && quantitat > 4, en un bucle sobre 7 sessions i 1811 vendes atures exactament al cas que investigues (les seves variants són el comptador d'encerts, que atura a la crida número 50, i l'expressió de registre). El de registre (logpoint) no atura res: imprimeix un missatge amb interpolació, Venent {quantitat} de {sessio.id}, en queden {sessio.lliures}, cada vegada que hi passa. És un console.log que no toca el codi font, perfecte quan el problema només apareix sota concurrència i aturar l'execució el faria desaparèixer.
Els controls són continuar (F5, segueix fins al pròxim punt), passar per sobre (F10, executa la línia sense entrar a les funcions que cridi), entrar (F11) i sortir (Shift+F11, acaba la funció actual i torna a qui l'ha cridat).
I els quatre panells: Variables mostra l'àmbit local, el de tancament i el global al punt on estàs aturat, i és on descobreixes que req.body és undefined; Inspecció segueix expressions concretes a cada pas (sessio.lliures, req.usuari.rol); Pila de crides reconstrueix com s'ha arribat fins aquí i hi pots fer clic a qualsevol marc per veure'n les variables, que sol ser on és la fallada real; i la Consola de depuració avalua qualsevol expressió en el context actual.
// Escrit a la consola de depuracio, amb l'execucio aturada:
sessio.lliures // 2
sessio.lliures >= req.body.quantitat // false <-- aqui hi ha el 409
req.usuari.rol = 'administrador' // canviar en viu i continuar per veure que passaAquesta última línia és una capacitat enorme: comprovar una hipòtesi sense editar, desar i reiniciar.
Depurar codi asíncron
A Node, la meitat dels problemes passen després d'un await, i allà la depuració té una peculiaritat. La pila es trenca perquè quan registres un callback amb setTimeout o resols una promesa, la funció s'executa més tard, en un altre torn del bucle d'esdeveniments, i aleshores la pila original ja s'ha desmuntat. Per això una traça clàssica mostra tres marcs (Timeout._onTimeout, listOnTimeout, processTimers), cap d'útil: no diu qui va programar aquell timeout.
Les traces asíncrones són la solució. Node manté informació que permet reconstruir la cadena lògica a través dels await, i DevTools i VS Code la mostren amb separadors:
Error: Aforament insuficient
at Sessio.vendre (/app/src/domini/sessio.js:58:11)
at comprarEntrades (/app/src/serveis/comandes.js:34:19)
--- await ---
at crearComanda (/app/src/controladors/comandes.js:22:24)
--- await ---
at Layer.handle (/app/node_modules/express/lib/router/layer.js:95:5)Els marcadors --- await --- separen els trams asíncrons i es llegeixen de dalt a baix com a passat cap enrere: l'error s'ha llançat a vendre, cridada des de comprarEntrades, que va ser esperada des de crearComanda.
Quatre consells pràctics. async/await produeix traces molt millors que els callbacks imbricats, una raó més per preferir-lo. No facis servir mai catch buits: esborren l'única pista que tenies. Preserva la causa en reembolcallar un error, que és el que fa útil una jerarquia com la nostra. I captura els rebuigs no gestionats a l'arrencada perquè no es perdin mai en silenci:
try {
await repositoriComandes.guardar(comanda);
} catch (error) {
// L'opcio cause encadena la traca original: sense ella, la perds
throw new ErrorDAplicacio("No s'ha pogut guardar la comanda", {
codi: 'ERROR_PERSISTENCIA', cause: error,
});
}
// src/servidor.js
process.on('unhandledRejection', (rao) => {
console.error('Promesa rebutjada sense gestionar:', rao);
process.exit(1); // fallar rapid: un estat desconegut no es segur
});Llegir una traça d'error de veritat
Una traça real és plena de soroll. En dissecarem una:
ValidationError: Comanda validation failed: linies.0.quantitat: Path `quantitat` (7)
is more than maximum allowed value (6).
at model.Document.invalidate (/app/node_modules/mongoose/lib/document.js:3241:32)
at /app/node_modules/mongoose/lib/schemaType.js:1368:9
at process.processTicksAndRejections (node:internal/process/task_queues.js:77:11)
at async repositoriComandes.crear (/app/src/repositoris/comandes.js:47:20)
at async comprarEntrades (/app/src/serveis/comandes.js:61:19)
at async crearComanda (/app/src/controladors/comandes.js:22:24)Es llegeix en aquest ordre. El tipus i el missatge primer: un ValidationError de Mongoose amb el camp exacte (linies.0.quantitat), el valor rebut (7) i la regla violada (màxim 6); el 80 % de les vegades la primera línia ja ho diu tot. Després salta el soroll de node_modules i node:internal, perquè no arreglaràs Mongoose. Tot seguit busca el primer marc de /app/src/ comptant des de dalt —repositoris/comandes.js:47—, que és on el teu codi ha provocat l'error, i continua baixant pels teus marcs per reconstruir el camí: repositori ← servei ← controlador. Diagnòstic: algú ha demanat 7 entrades i la validació de zod no l'ha aturat abans d'arribar al model, així que la fallada real no és de Mongoose sinó un forat a la validació d'entrada, i probablement el client rebi un 500 en comptes del 422 que li correspon.
Dues eines per controlar les traces. Node desa 10 marcs per defecte, curt per a cadenes asíncrones llargues: s'amplia amb node --stack-trace-limit=50 o amb Error.stackTraceLimit = 50. I Error.captureStackTrace(this, this.constructor) al constructor d'ErrorDAplicacio elimina de la traça els marcs del mateix constructor, un detall petit que fa que els teus errors assenyalin directament el codi que els ha llançat.
Errors freqüents i el seu diagnòstic
Tots aquests ja han aparegut al llarg del curs. Aquesta taula és la teva referència de consulta:
| Error | Causa habitual a Escena Viva | Com diagnosticar-lo |
|---|---|---|
ERR_HTTP_HEADERS_SENT |
Un res.json() sense return seguit d'un next(error) |
Punt d'interrupció al segon enviament; busca el return que falta |
EADDRINUSE |
Un npm run dev oblidat, o dues proves amb port fix |
lsof -i :3000 o ss -ltnp i matar el procés; port efímer a les proves |
ECONNREFUSED |
Mongo o PostgreSQL aturats, o URI equivocada al .env |
Comprova el servei i la variable; imprimeix configuracio en arrencar |
ETIMEDOUT |
Proveïdor extern lent, tallafoc, AbortSignal.timeout curt |
Puja el temps per confirmar-ho; després revisa la xarxa, no el timeout |
MODULE_NOT_FOUND |
Extensió oblidada, ruta relativa malament, node_modules desactualitzat |
El missatge porta la ruta buscada: compara-la caràcter a caràcter |
unhandledRejection |
await oblidat, o async sense propagar |
Registra el gestor de process; la traça n'indica el punt |
CastError |
GET /api/esdeveniments/no-existeix amb Mongoose |
Validar el paràmetre amb zod i retornar 400 o 404, no un 500 |
E11000 duplicate key |
Dos usuaris amb el mateix correu; llavor executada sense netejar | El missatge porta l'índex; tradueix-lo a ConflicteDEstat al repositori |
| El procés no acaba | Connexió a Mongo sense tancar, setInterval viu, servidor escoltant |
why-is-node-running (a sota) |
| La petició es queda penjada | El next() oblidat del mòdul 6 |
Punt de registre a cada middleware: l'últim que imprimeix és el culpable |
Els dos últims mereixen desenvolupament. Amb "exit": false al .mocharc.json, el procés que no acaba apareix tan bon punt t'oblides de tancar alguna cosa:
// test/ajudes/preparacio.js, temporalment mentre investigues
const perQueContinuaViu = require('why-is-node-running');
exports.mochaHooks = {
async afterAll() {
await desconnectar();
setTimeout(() => perQueContinuaViu(), 1000).unref();
},
};Imprimeix cada handle i request obert amb la traça del lloc on es va crear, i sol ser una de tres coses: la connexió de Mongoose, un setInterval d'una memòria cau o d'un netejador de tokens, o un http.Server creat a mà. L'alternativa sense instal·lar res és imprimir process._getActiveHandles().length en un after: és API interna i sense documentar, però per depurar serveix.
Per a la petició penjada, el diagnòstic més ràpid és un middleware temporal de traçat, app.use(tracar('json')), app.use(tracar('autenticar')), etcètera, on tracar(etiqueta) imprimeix [${req.idPeticio}] passa per ${etiqueta} i crida next(). L'últim tracar que apareix a la consola assenyala el middleware següent com a culpable.
Fuites de memòria
Una fuita és memòria que es reté i ja no es fa servir. En un script no importa; en un servidor que corre setmanes, acaba en un reinici per falta de memòria. Els símptomes: la memòria del procés creix de manera sostinguda i no baixa després de les hores vall, el rendiment es degrada a poc a poc perquè el recol·lector treballa cada vegada més, i finalment arriba JavaScript heap out of memory.
El primer diagnòstic no necessita eines: un setInterval temporal que imprimeixi process.memoryUsage() cada minut. Dels seus quatre camps, rss és la memòria total del procés, heapTotal el que ha reservat V8, external els Buffers, i heapUsed és el que importa. La clau no és el valor absolut sinó la tendència durant hores: que pugi i baixi és normal, que només pugi no ho és.
El diagnòstic seriós és comparar dues instantànies del munt. Arrenca amb node --inspect, ves a chrome://inspect, pestanya Memory, i pren la instantània A. Exercita l'aplicació repetint diverses vegades el mateix cicle —compres, llistats, entrades de sessió— i pren la instantània B. Selecciona B i canvia la vista a Comparison contra A: la columna Delta mostra quins tipus d'objecte han crescut sense alliberar-se, i si després de deu cicles idèntics veus 10.000 objectes Sessio més o 500 oients més, allà hi ha la teva fuita. La secció Retainers et diu qui manté viva la referència, que és la informació que realment necessites. També la pots generar des del codi amb v8.writeHeapSnapshot(), útil en un servidor sense navegador: retorna la ruta d'un fitxer .heapsnapshot que després es descarrega i es carrega a DevTools.
Les causes típiques, totes ja vistes en aquest curs:
| Causa | Com passa a Escena Viva | Solució |
|---|---|---|
| Oients no desregistrats (M2) | Cada petició fa gestorDeVendes.on('venda-registrada', ...) i ningú no fa off |
Fer servir once o off explícit; vigilar MaxListenersExceededWarning |
| Memòria cau sense límit (M4) | La memòria cau de canvi-divises.js desa cada divisa i no caduca mai |
Límit de mida (LRU) i caducitat per entrada |
| Variables de mòdul que acumulen | Un array d'«últimes peticions» al qual només es fa push |
Mida màxima, o moure-ho a Redis (mòdul 10) |
| Tancaments que retenen | Un callback que captura el req sencer i viu en un temporitzador |
Capturar només el que cal (req.idPeticio, no req) |
| Temporitzadors no cancel·lats | setInterval per sessió d'usuari |
clearInterval en acabar; unref() quan escaigui |
L'avís MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 venda-registrada listeners added és un regal de Node: t'està dient exactament on has de mirar. No el silenciïs apujant setMaxListeners; investiga per què s'afegeixen.
Depurar en producció sense aturar el servei
En producció no pots posar un punt d'interrupció: aturaries les peticions de tots els usuaris. Les tècniques són unes altres.
1. Registres estructurats amb identificador de petició. És, de bon tros, l'eina més valuosa. El middleware id-peticio.js del mòdul 6 assigna un identificador únic a cada petició i registre-peticions.js l'inclou a cada línia:
{"nivell":"info","idPeticio":"9f3c1e","metode":"POST","ruta":"/api/comandes","usuariId":"usr-001","missatge":"compra iniciada"}
{"nivell":"warn","idPeticio":"9f3c1e","codi":"AFORAMENT_INSUFICIENT","sessioId":"ses-001-1","lliures":2,"sollicitades":4}
{"nivell":"info","idPeticio":"9f3c1e","estat":409,"duracioMs":91}Amb aquest idPeticio reconstrueixes el recorregut complet d'una petició concreta entre milers de línies simultànies: quan la Lucía escrigui dient que no ha pogut comprar a les 18:22, tens la seva història sencera filtrant per un camp. El monitoratge i l'agregació d'aquests registres es tanquen al mòdul 11. Regla d'or: no registris mai dades personals, contrasenyes, tokens ni capçaleres Authorization, perquè un registre és un fitxer que moltes persones poden llegir i que sovint s'envia a un tercer.
2. Activar l'inspector amb un senyal, temporalment. kill -USR1 <pid> fa que Node obri l'inspector al 9229 sense reiniciar el procés, i t'hi connectes per un túnel SSH: ssh -L 9229:localhost:9229 usuari@servidor, i després chrome://inspect a la teva màquina local veu el procés remot.
3. No deixis mai --inspect obert en un servidor públic. Això és un advertiment de seguretat seriós, no una recomanació d'estil: el port de l'inspector no té autenticació, així que qualsevol que hi arribi pot avaluar codi arbitrari al teu procés, llegir memòria —inclosos els secrets JWT i les contrasenyes de la base de dades— i modificar el comportament en viu. És execució remota de codi servida en safata. Les regles mínimes: mai --inspect=0.0.0.0:9229 (per defecte escolta només a 127.0.0.1, deixa-ho així), accedeix-hi sempre per túnel SSH, tanca'l tan bon punt acabis, i que el tallafoc bloquegi el 9229 des de l'exterior com a xarxa de seguretat.
4. Bolcats de memòria sota demanda, mitjançant un punt d'entrada intern protegit per rol d'administrador i accessible només des de la xarxa privada que cridi v8.writeHeapSnapshot(). Permet investigar una fuita en producció sense tocar el servei.
Convé deixar clara la frontera, perquè la confusió és habitual:
| Depuració (aquest mòdul) | Perfilatge (mòdul 10) | |
|---|---|---|
| Pregunta | Per què fa una cosa incorrecta? | Per què triga tant? |
| Símptoma | Error, resultat erroni, penjada | Lentitud, poca capacitat |
| Eines | Inspector, punts d'interrupció, traces, registres | Perfils de CPU, flamegraphs, autocannon, clinic |
| Resultat | Una fallada localitzada i una prova que la reprodueix | Un coll d'ampolla quantificat |
Mètode de depuració
Les eines sense mètode produeixen tardes perdudes. Aquest és el procediment, en ordre.
1. Reproduir. Una fallada que no pots reproduir no la pots arreglar; com a molt pots canviar coses fins que sembli que desapareix. Reuneix petició exacta, usuari i rol, dades implicades, hora i idPeticio; si és intermitent, executa-la en bucle fins que caigui.
2. Aïllar. Redueix fins al mínim que continua fallant: falla sense autenticació?, amb una altra sessió?, només amb evt-002? Cada resposta elimina mig bosc.
3. Formular una hipòtesi falsable. No «alguna cosa estranya passa amb l'aforament», sinó «crec que vendre() compara amb < en lloc de <= i per això rebutja l'última entrada». Una hipòtesi que no es pot refutar no serveix de res.
4. Comprovar-la amb un punt d'interrupció, un punt de registre o una consulta. Si era falsa, no la retorcis: formula'n una altra. Aferrar-se a una hipòtesi equivocada és la causa número u de les depuracions llargues.
5. Bisecció amb git bisect quan sàpigues que abans funcionava:
git bisect start
git bisect bad # el commit actual falla
git bisect good v1.4.0 # aquesta versio funcionava
git bisect run npm run test:unitat # automatic: Git fa servir el codi de sortida
git bisect resetAmb 1000 commits entre el bo i el dolent, git bisect troba el culpable en 10 passos, i amb run te'n vas a fer un cafè i tornes amb el commit exacte. Fixa't en el que implica: git bisect automàtic només funciona si tens proves. És un altre benefici d'aquest mòdul que no es veu fins que el necessites.
6. Escriure una prova que reprodueixi la fallada, abans d'arreglar-la. Aquest pas tanca el cercle del mòdul sencer i és innegociable. L'ordre importa: escrius la prova i ha de fallar —si passa, no has entès la fallada i estàs a punt d'arreglar una altra cosa—, després arregles el codi, la prova passa, i per últim executes la bateria completa per comprovar que l'arranjament no ha trencat res més.
// Incidencia 481: una comanda d'exactament les ultimes localitats retornava 409.
// Aquesta prova va fallar en vermell abans de l'arranjament (vendre comparava amb < i no <=).
it('permet comprar exactament les ultimes localitats lliures', async () => {
const lucia = await comAssistent();
await ajustarAforament('ses-001-1', { lliures: 2 });
const { body } = await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 2 }).expect(201);
expect(body.entrades).to.have.lengthOf(2);
const sessio = await request(app).get('/api/sessions/ses-001-1').expect(200);
expect(sessio.body.lliures).to.equal(0);
expect(sessio.body.exhaurida).to.be.true;
});Tres coses guanyes amb aquesta prova: demostres que has entès la fallada, garanteixes que no tornarà mai i deixes documentat un cas límit que a ningú no se li havia acudit. Una fallada en producció és cara; malbaratar-la sense convertir-la en una prova és llençar els diners.
Errors Comuns i Consells
- Depurar sense reproduir. Canviar codi fins que «sembla que ja va» no arregla res: ho amaga fins al pitjor moment possible.
- Oblidar
--timeout 0en depurar proves. Mocha avorta la prova mentre estàs aturat llegint variables i et penses que el depurador està trencat. - No fer servir
skipFiles. Prems F11 i acabes dins d'express/lib/router/layer.jssense saber com sortir-ne. catchbuits. Esborren l'única informació que tenies; registra sempre, encara que sigui en nivelldebug.- Perdre la causa en reembolcallar errors. Fes servir
{ cause: error }: la traça original val més que un missatge bonic. - Silenciar
MaxListenersExceededWarning. És un detector de fuites gratuït; apujar el límit és tapar l'avís d'incendi. - Deixar
--inspecten un servidor accessible. És execució remota de codi sense autenticació. Túnel SSH sempre. - Registrar tokens o contrasenyes. Un registre és un fitxer que molta gent llegeix i que sol sortir de la teva infraestructura.
- Consell: quan una prova d'integració falli amb 500, imprimeix
resposta.bodyi busca l'idPeticioa la sortida del servidor: tens la història completa en dos passos. I aprèn bé els punts d'interrupció condicionals, que és l'habilitat que més temps estalvia de tota la lliçó.
Exercicis
Exercici 1: diagnosticar sense executar
Per a cada símptoma, indica la causa més probable, l'eina amb què ho confirmaries i l'arranjament:
npm testimprimeix74 passingi el procés es queda penjat sense tornar l'indicador.POST /api/comandesrespon 500 ambCannot set headers after they are sent to the client.GET /api/esdeveniments/holaretorna 500 en lloc de 404.- El servidor fa cinc dies que està amunt i
heapUsedha passat de 90 MB a 780 MB. - Una prova d'integració passa sola i falla dins de la bateria completa.
Exercici 2: depurar una prova amb VS Code
Agafa la prova de concurrència de 09-04. Configura .vscode/launch.json amb l'entrada «Mocha: el fitxer obert», col·loca un punt d'interrupció condicional dins del repositori de comandes que només s'activi quan lliures < 2, i descriu què veuries al panell de pila de crides i al de variables. Explica per què --timeout 0 és imprescindible aquí.
Exercici 3: de la fallada a la prova
Un organitzador de la Sala Bóveda informa que l'informe d'evt-002 mostra ingressosCentims: NaN des d'ahir. Dissenya el procés complet: com reproduir-ho, com aïllar-ho, dues hipòtesis falsables, com faries servir git bisect, i escriu la prova que reprodueix la fallada (ha de fallar abans de l'arranjament).
Solucions
Exercici 1. (1) Un handle obert, gairebé segur la connexió a Mongo sense tancar; confirma-ho amb why-is-node-running a l'afterAll i arregla-ho amb await desconnectar() al ganxo arrel. (2) Doble resposta: un gestor fa res.json(...) i després crida next(error), o hi ha un await després de respondre que llança; punt d'interrupció al gestorDErrors i mira la pila; l'arranjament és return res.json(...). (3) CastError de Mongoose: hola no és un ObjectId vàlid i l'error arriba com a desconegut, que ESTAT_PER_CODI tradueix a 500; valida el paràmetre amb zod o tradueix CastError a RecursNoTrobat al repositori. (4) Fuita de memòria: dues instantànies del munt comparades després de cicles idèntics, amb els oients acumulats a GestorDeVendes i la memòria cau sense límit de canvi-divises.js com a principals sospitosos. (5) Contaminació entre proves: un stub sense restaurar, un rellotge fals viu o dades que una altra prova ha deixat; confirma-ho executant la bateria en ordre invers i arregla-ho amb sinon.restore() al ganxo arrel i neteja a l'afterEach.
Exercici 2. El punt d'interrupció condicional es col·loca a la línia del repositori on es comprova l'aforament, amb la condició sessio.lliures < 2. Al panell de pila de crides veuries, de dalt a baix, el mètode del repositori, comprarEntrades, el controlador crearComanda i diversos marcs d'Express separats per marcadors --- await ---; fent clic al marc del controlador veuries req.usuari i req.dadesValidades d'aquella petició concreta. Al panell de variables veuries sessio.lliures amb el valor exacte a l'instant en què una de les deu compres simultànies troba l'aforament gairebé exhaurit, i podries comprovar a la consola si la comparació amb quantitat dona el que esperaves.
--timeout 0 és imprescindible per una raó específica: la prova llança deu peticions amb Promise.all, i mentre estàs aturat en una les altres nou continuen esperant; amb el timeout normal de 5000 ms, Mocha avortaria la prova sencera als cinc segons, tancant l'aplicació sota els teus peus.
Exercici 3. Reproduir: cridar GET /api/esdeveniments/evt-002/informe amb un organitzador d'org-boveda a la base de proves sembrada; si no es reprodueix, copiar les dades reals d'evt-002, perquè aleshores la fallada és a les dades i no al codi. Aïllar: provar amb evt-001 i evt-003; si només falla evt-002 —el de les tres sessions—, calcular l'informe sessió a sessió per trobar quina produeix NaN. Hipòtesis falsables: que una sessió tingui preuCentims nul i el reduce propagui NaN (perquè undefined * 2 és NaN i el NaN contamina tota la suma), o que l'informe sumi preuEuros en lloc de preuCentims. Bisecció: com que «des d'ahir» acota la finestra, git bisect start, bad HEAD, good <commit d'abans-d'ahir> i git bisect run npm run test:unitat.
it('calcula els ingressos encara que una sessio no tingui preu assignat', () => {
const esdeveniment = crearEsdeveniment({
id: 'evt-002',
sessions: [
{ venudes: 120, preuCentims: 1800 },
{ venudes: 95, preuCentims: 1800 },
{ venudes: 60, preuCentims: undefined }, // la sessio problematica
],
});
expect(Number.isNaN(esdeveniment.ingressosCentims)).to.be.false;
expect(esdeveniment.ingressosCentims).to.equal(387000); // (120 + 95) * 1800
});Aquesta prova falla en vermell abans de l'arranjament, que té dues parts i totes dues importen: tractar el preu absent com a zero al càlcul (s.preuCentims ?? 0) i fer que el model exigeixi preuCentims, perquè la dada invàlida no hi pugui tornar a entrar. Arreglar només el símptoma deixaria la porta oberta.
Conclusió
Amb aquesta lliçó es tanca el mòdul 9, i amb ell es tanca una escletxa que feia des del mòdul 5 que estava oberta.
Escena Viva ja no descansa sobre la confiança. npm test està verd i significa alguna cosa: el domini calcula bé l'aforament i els totals en cèntims, la matriu de permisos està recorreguda cel·la per cel·la —aquell if invertit a potGestionarEsdeveniment que obriria el catàleg sencer ara posa diverses proves en vermell—, el conversor de divises degrada amb elegància quan el proveïdor cau, l'API respon 401, 403, 404, 409, 422 i 400 amb el format d'error acordat, un assistent no pot veure la comanda d'un altre, i deu compres simultànies per cinc butaques venen exactament cinc butaques. La cobertura està mesurada amb llindars que només pugen, els scripts estan organitzats, i el projecte es pot executar sol en un servidor d'integració contínua. I quan alguna cosa falli —perquè alguna cosa fallarà—, saps reproduir-la, aïllar-la, posar-hi un punt d'interrupció condicional, llegir-ne la traça entre el soroll de node_modules, trobar el commit culpable amb git bisect i convertir-la en una prova que n'impedeixi el retorn.
Aquesta és la xarxa de seguretat. A partir d'ara pots canviar el codi sense por, que era exactament la promesa amb què vam obrir la primera lliçó.
I tanmateix, l'aplicació té un problema del qual les proves no diuen ni una paraula, perquè no és un problema de correcció: és correcta, però és lenta i desaprofita la màquina. Escena Viva fa servir un sol nucli mentre el servidor en té vuit, i a l'estrena del Festival de Jazz de Primavera tots els processos es barallaran per aquell únic fil. Recalcula el mateix catàleg centenars de vegades per minut per retornar sempre el mateix. I quan genera el PDF d'una entrada, bloqueja el bucle d'esdeveniments el temps suficient perquè les altres peticions es quedin esperant a la cua.
Al mòdul 10, Temes Avançats, ataquem justament això: cluster per fer servir tots els nuclis, worker threads per treure la generació de PDF del fil principal, Redis per posar el catàleg a la memòria cau i encuar la feina pesada, optimització del rendiment amb perfils de CPU, flamegraphs i proves de càrrega —la frontera que hem respectat en aquest mòdul—, i per últim el disseny d'APIs RESTful madures i una introducció a GraphQL.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
