La lliçó anterior va acabar amb una llista incòmoda: el wifi del taller que cau a mitja POST, l'API que triga quinze segons, el 503 d'un desplegament, les vuit peticions que dispara l'Iván en teclejar al cercador i que tornen desordenades. js/dades/api-tasques.js funciona perfectament mentre tot va bé, i a la xarxa res va bé tota l'estona. Aquesta lliçó és la que converteix una demostració en una aplicació: aprendràs a classificar les fallades i decidir què fer amb cadascuna, a encapsular-les en un ErrorDeApi propi, a cancel·lar peticions amb AbortController —tancant el deute que vas deixar pendent a 06-04—, a imposar timeouts, a reintentar només el que és segur reintentar, a llançar peticions en paral·lel sense que una sola caiguda ho tombi tot, i a modelar els quatre estats que tota pantalla que parla amb una xarxa ha de saber mostrar. Acabaràs amb js/dades/http.js i amb una TaulerVista que sap dir «carregant», «això ha fallat, reintenta» i «aquí no hi ha res».
Contingut
- Les set fallades d'una petició
- Un error propi:
ErrorDeApi demanarJson: l'embolcall definitiuAbortControllera fons- Cancel·lar la petició anterior: el cercador
- Timeouts:
Promise.racedavant d'AbortSignal.timeout() AbortSignal.any(): combinar senyals- Reintents amb retrocés exponencial i jitter
- Idempotència: què es pot reintentar
- Paral·lel sense fragilitat:
allSettleddavant d'all - Els quatre estats de la interfície
- Avisos accessibles amb
aria-live - Optimistic UI: aplicar abans de confirmar
- Nómada Tasques:
js/dades/http.jsi la vista - Errors Habituals i Consells
- Exercicis
- Conclusió
- Les set fallades d'una petició
Abans d'escriure una línia, convé tenir el mapa complet. No totes les fallades mereixen el mateix tractament, i tractar-les igual produeix interfícies que menteixen («Error desconegut») o que insisteixen quan no han de fer-ho.
| Fallada | Què ha passat | Com es detecta | Reintentar? | Què dir a l'usuari |
|---|---|---|---|---|
| Xarxa caiguda | Sense connexió, wifi perdut | fetch rebutja amb TypeError |
Sí, amb retrocés | «Sense connexió. Reintentant…» |
| DNS / host inabastable | El domini no resol | fetch rebutja amb TypeError |
Sí, poques vegades | «No es pot contactar amb el servidor» |
| CORS bloquejat | El servidor no autoritza el teu origen | fetch rebutja amb TypeError; el motiu, només a la consola |
No | «Error de configuració» + avisar desenvolupament |
| 4xx del client | Dades malament, sense permís, no existeix | resposta.ok === false, status 400-499 |
No (llevat de 408 i 429) |
El missatge concret: «Falten hores», «Sessió caducada» |
| 5xx del servidor | El servidor ha fallat | resposta.ok === false, status 500-599 |
Sí | «El servidor no respon. Reintentant…» |
| Resposta no-JSON | Ha arribat HTML d'un proxy o d'un login | json() llança SyntaxError |
No | «Resposta inesperada del servidor» |
| Resposta lenta | El servidor triga massa | Res… fins que hi poses un timeout | Sí, una vegada | «Està trigant més del normal» |
Tres lectures d'aquesta taula:
- Només dues files fan rebutjar la promesa de
fetch: les fallades de transport (xarxa, DNS, CORS, cancel·lació). Tota la resta arriba com a resposta «normal» i cal mirar-la, tal com vas aprendre a 07-02. - La frontera 4xx / 5xx decideix si cal reintentar. Insistir davant d'un
400és garantia de tornar a fallar; insistir davant d'un503sol funcionar. - La resposta lenta no es detecta sola. És l'única fallada que tu has de provocar, posant-hi un límit. Sense timeout, una petició es pot quedar penjada indefinidament i la teva interfície mostrarà «Carregant…» per sempre.
Hi ha dos casos de 4xx que sí que es reintenten, i convé recordar-los: 408 Request Timeout i 429 Too Many Requests. El 429 a més sol portar una capçalera Retry-After que diu quants segons esperar; respectar-la és de bona educació i evita que et bloquegin.
- Un error propi:
ErrorDeApi
ErrorDeApiA 05-02 vas crear ErrorDeValidacio extends Error amb un camp camp, i a 06-07 aquell camp va permetre a la vista saber a quin <input> moure el focus. Aquí es repeteix exactament la mateixa jugada: un error que transporta la informació que la vista necessitarà per decidir què fer.
// js/model/errors.js — s'afegeix als que ja existien
export class ErrorDeApi extends Error {
/**
* @param {string} missatge Text llegible, apte per mostrar
* @param {object} dades { status, codi, url, detall, causa }
*/
constructor(missatge, { status = 0, codi = 'desconegut', url = '', detall = null, causa = null } = {}) {
super(missatge);
this.name = 'ErrorDeApi';
this.status = status; // 0 si ni tan sols hi va haver resposta HTTP
this.codi = codi; // 'xarxa' | 'timeout' | 'cancel·lat' | 'client' | 'servidor' | 'format'
this.url = url;
this.detall = detall; // el cos de l'error, si el servidor n'ha enviat un
this.causa = causa; // l'error original, per depurar
}
/** Val la pena tornar-ho a intentar? */
get reintentable() {
if (this.codi === 'xarxa' || this.codi === 'timeout' || this.codi === 'servidor') return true;
return this.status === 408 || this.status === 429;
}
/** Cal demanar a l'usuari que iniciï sessió una altra vegada? */
get requereixSessio() {
return this.status === 401;
}
descriure() {
return `[${this.name} ${this.status} ${this.codi}] ${this.message}`;
}
}El que fa valuós aquest error no és que sigui una classe: és que el coneixement sobre reintents viu en un sol lloc. La vista no ha de saber que un 429 es reintenta i un 403 no; pregunta error.reintentable i ja està. És el mateix principi de 06-07: les regles viuen on toca, i qui les consumeix només pregunta.
Un apunt d'ES2022 que encaixa aquí: Error accepta una opció cause estàndard, new Error('…', { cause: original }). Nosaltres fem servir un camp propi causa per coherència amb ErrorDeDades, però conèixer la forma estàndard t'evitarà confusions en llegir codi aliè.
demanarJson: l'embolcall definitiu
demanarJson: l'embolcall definitiuAra la funció central de tota la capa de xarxa. Escrita una vegada, feta servir per tots els mètodes de l'API.
// js/dades/http.js
import { ErrorDeApi } from '../model/errors.js';
/**
* Fa una petició i retorna el JSON, o llança SEMPRE un ErrorDeApi classificat.
* Mai deixa escapar un TypeError ni un SyntaxError sense traduir.
*/
export async function demanarJson(url, opcions = {}) {
let resposta;
// ── 1 · Fallades de transport: les úniques que fan rebutjar fetch ──
try {
resposta = await fetch(url, opcions);
} catch (error) {
if (error.name === 'AbortError') {
throw new ErrorDeApi('Petició cancel·lada.', { codi: 'cancel·lat', url, causa: error });
}
if (error.name === 'TimeoutError') {
throw new ErrorDeApi('El servidor ha trigat massa.', { codi: 'timeout', url, causa: error });
}
// TypeError: xarxa caiguda, DNS, CORS. El navegador no distingeix quin per no filtrar informació.
throw new ErrorDeApi("No s'ha pogut contactar amb el servidor.", { codi: 'xarxa', url, causa: error });
}
// ── 2 · Respostes d'error: 4xx i 5xx ──
if (!resposta.ok) {
const detall = await llegirDetall(resposta);
const codi = resposta.status >= 500 ? 'servidor' : 'client';
const missatge = detall?.missatge ?? detall?.message ?? `Error ${resposta.status} del servidor.`;
throw new ErrorDeApi(missatge, { status: resposta.status, codi, url: String(url), detall });
}
// ── 3 · Èxit sense cos ──
if (resposta.status === 204 || resposta.headers.get('content-length') === '0') return null;
// ── 4 · Èxit amb cos: és realment JSON? ──
const tipus = resposta.headers.get('content-type') ?? '';
if (!tipus.includes('json')) {
const text = await resposta.text().catch(() => '');
throw new ErrorDeApi(`S'esperava JSON i ha arribat "${tipus}".`, {
status: resposta.status, codi: 'format', url: String(url), detall: text.slice(0, 200)
});
}
try {
return await resposta.json();
} catch (error) {
throw new ErrorDeApi('La resposta no és JSON vàlid.', {
status: resposta.status, codi: 'format', url: String(url), causa: error
});
}
}
/** Intenta llegir el cos de l'error com a JSON; si no pot, com a text; si no, null. */
async function llegirDetall(resposta) {
const copia = resposta.clone(); // ← clonar ABANS de llegir (07-02)
try {
return await resposta.json();
} catch {
return await copia.text().catch(() => null);
}
}Atura't en quatre punts:
- La funció té un únic tipus de sortida d'error. Qui la cridi només necessita conèixer
ErrorDeApi. Res de comprovar si és unTypeError, unSyntaxErroro un objecteResponse. - El
clone()de l'apartatllegirDetallés imprescindible: sijson()falla a mitges, el flux del cos ja està consumit itext()no el podria llegir. - Es comprova el
content-type. Aquest és el que salva dels diagnòstics impossibles: un proxy corporatiu que retorna la seva pàgina d'inici de sessió amb estat 200, i unjson()que peta amb unSyntaxError: Unexpected token '<'. Amb aquesta comprovació, el missatge diu exactament què va passar. status: 0significa «no hi va haver resposta HTTP». És la convenció que distingeix una fallada de transport d'una d'aplicació.
AbortController a fons
AbortController a fonsA 06-04 vas fer servir { signal } per desconnectar oients d'esdeveniments de cop, i va quedar dit que s'aprofundiria aquí. AbortController és un mecanisme general de cancel·lació: un objecte amb dues peces.
const controlador = new AbortController();
console.log(controlador.signal); // AbortSignal { aborted: false, reason: undefined }
console.log(controlador.signal.aborted); // false
controlador.abort(); // ← es dispara la cancel·lació
console.log(controlador.signal.aborted); // true
console.log(controlador.signal.reason); // DOMException: signal is aborted without reason| Peça | Qui la té | Per a què |
|---|---|---|
controller |
Qui decideix cancel·lar | Crida .abort(motiu?) |
controller.signal |
Qui executa l'operació | Es passa a fetch, a addEventListener… |
signal.aborted |
— | true si ja s'ha cancel·lat |
signal.reason |
— | El motiu passat a abort(), o un AbortError per defecte |
signal.throwIfAborted() |
— | Llança si ja està cancel·lada; útil en bucles llargs |
Esdeveniment abort a signal |
— | Per netejar recursos propis |
Aplicat a fetch:
const controlador = new AbortController();
// Cancel·lar als 3 segons, passi el que passi
setTimeout(() => controlador.abort(), 3000);
try {
const resposta = await fetch(url, { signal: controlador.signal });
const dades = await resposta.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Cancel·lada a propòsit, no és una fallada'); // ← NO és un error per mostrar
} else {
throw error;
}
}Distingir AbortError d'un error real és obligatori. Quan tu canceles una petició perquè ja no t'interessa, mostrar un missatge d'error a l'usuari és un bug: no ha fallat res, has estat tu. Aquell if (error.name === 'AbortError') apareix en tota aplicació seriosa, i per això demanarJson el tradueix a codi: 'cancel·lat', que la vista sap ignorar.
Tres detalls que convé tenir clars:
- Un controlador és d'un sol ús. Un cop avortat, el seu senyal queda avortat per sempre. Per a l'operació següent, un controlador nou.
- Pots passar un motiu:
controlador.abort(new Error("L'usuari ha canviat de pàgina")). Apareixerà asignal.reasoni ajuda molt a depurar. - Un mateix senyal pot cancel·lar diverses coses. Un
fetch, tresaddEventListeneri unsetIntervalpropi, tots amb el mateixsignal: unabort()els desconnecta tots. És el patró de neteja d'una vista que es destrueix.
/** Tot el que registra aquesta vista mor amb un únic abort(). */
function muntarPanell(contenidor, { signal }) {
contenidor.addEventListener('click', enFerClic, { signal });
window.addEventListener('resize', enRedimensionar, { signal });
const id = setInterval(refrescar, 30000);
signal.addEventListener('abort', () => clearInterval(id)); // neteja del que no accepta signal
}
const controlador = new AbortController();
muntarPanell($('#panell'), { signal: controlador.signal });
// …més tard…
controlador.abort(); // se'n van els tres oients i l'interval, de cop
- Cancel·lar la petició anterior: el cercador
Aquest és el cas on la cancel·lació deixa de ser un adorn. L'Iván escriu «serigrafia» al cercador del tauler. Sense cura, això són deu pulsacions i deu peticions, i les respostes no tornen en ordre: la de «serig» pot arribar després de la de «serigrafia» i sobreescriure la pantalla amb resultats equivocats. És la race condition clàssica de les interfícies.
sequenceDiagram
participant U as Iván
participant V as Vista
participant A as API
U->>V: tecleja "serig"
V->>A: GET ?text=serig
U->>V: tecleja "serigrafia"
V->>A: GET ?text=serigrafia
A-->>V: resposta de "serigrafia" (ràpida)
V->>V: pinta 3 resultats ✅
A-->>V: resposta de "serig" (lenta)
V->>V: pinta 11 resultats ❌ obsoleta!
La solució combina dues eines que ja coneixes: el debounce de 03-04 per no disparar a cada tecla, i AbortController perquè la petició anterior no arribi a retornar res.
// js/dades/api-tasques.js
let controladorCerca = null;
export async function cercarTasques(text) {
controladorCerca?.abort(); // ← mata l'anterior, si n'hi havia
controladorCerca = new AbortController();
try {
const url = construirUrl('/tasques', { q: text });
const planes = await demanarJson(url, { signal: controladorCerca.signal });
return planes.map((d) => Tasca.desDeJSON(d));
} catch (error) {
if (error.codi === 'cancel·lat') return null; // ← null significa "ignora'm"
throw error;
}
}// js/vista/controlador.js
import { debounce } from '../util/temps.js';
const cercar = debounce(async (text) => {
const resultat = await cercarTasques(text);
if (resultat === null) return; // ha estat cancel·lada: la substitueix una de més nova
vista.actualitzar({ tauler: new Tauler('Taller Nómada', resultat) });
}, 300);
$('#cercador').addEventListener('input', (esdeveniment) => cercar(esdeveniment.target.value));El debounce redueix deu peticions a una o dues; l'abort garanteix que, de les que s'arriben a llançar, només l'última pinta. Les dues peces juntes, no una de sola.
- Timeouts:
Promise.race davant d'AbortSignal.timeout()
Promise.race davant d'AbortSignal.timeout()fetch no té timeout. Una petició pot esperar minuts si el servidor accepta la connexió i no contesta mai. Hi ha dues maneres d'imposar un límit.
La clàssica, amb Promise.race (05-06):
/** Fa córrer la promesa contra un rellotge: guanya la primera que se salda. */
function ambLimit(promesa, ms) {
const rellotge = new Promise((_, rebutjar) => {
setTimeout(() => rebutjar(new Error(`Temps esgotat després de ${ms} ms`)), ms);
});
return Promise.race([promesa, rellotge]);
}
const dades = await ambLimit(fetch(url).then((r) => r.json()), 5000);Funciona, però té un defecte greu: la petició continua en marxa. Promise.race decideix qui guanya la cursa, no atura el perdedor. El servidor continua processant, els bytes continuen baixant i la connexió continua ocupada. Amb molts timeouts seguits, acumules peticions zombis.
La moderna, amb AbortSignal.timeout(), que cancel·la de debò:
const dades = await demanarJson(url, { signal: AbortSignal.timeout(5000) });
// Si passen 5 s, la petició s'AVORTA i l'error té name 'TimeoutError'Una línia, i la petició es talla d'arrel. La comparació:
Promise.race |
AbortSignal.timeout(ms) |
|
|---|---|---|
| Talla la petició de debò | No, continua en marxa | Sí |
| Allibera la connexió | No | Sí |
| Codi necessari | Una funció auxiliar | Res, és natiu |
| Nom de l'error | El que hi posis tu | TimeoutError |
| Serveix per a qualsevol promesa | Sí (càlculs, altres APIs) | Només per al que accepti signal |
| Disponibilitat | Universal | Navegadors moderns |
La conclusió pràctica: fes servir AbortSignal.timeout() per a peticions de xarxa, i reserva Promise.race per posar límit a promeses que no accepten senyals. Conèixer les dues importa perquè Promise.race continua sent l'eina general.
I un advertiment sobre el valor: un timeout massa curt converteix una xarxa lenta en una fallada. Xifres raonables per començar: 5 s per a lectures que bloquegen la pantalla, 10-15 s per a escriptures que l'usuari ja ha confirmat, i més marge per a pujades de fitxers.
AbortSignal.any(): combinar senyals
AbortSignal.any(): combinar senyalsDe vegades una petició s'ha de cancel·lar per dos motius diferents: perquè ha vençut el temps, o perquè l'usuari se n'ha anat de la pantalla. AbortSignal.any() fabrica un senyal que s'avorta així que qualsevol dels que li passes s'avorta.
const controladorVista = new AbortController(); // s'avorta en desmuntar la vista
const senyal = AbortSignal.any([
controladorVista.signal, // l'usuari se n'ha anat
AbortSignal.timeout(8000) // o ha trigat massa
]);
const tasques = await demanarJson(url, { signal: senyal });És la contrapartida de Promise.any de 05-06, però per a cancel·lacions: la primera que es dispari guanya. Aplicat al projecte:
// js/dades/http.js
/** Senyal combinat: timeout propi + cancel·lació externa (per exemple, en desmuntar). */
export function senyalAmb(ms, externa) {
const propis = [AbortSignal.timeout(ms)];
if (externa) propis.push(externa);
return AbortSignal.any(propis);
}Si el teu navegador objectiu no té AbortSignal.any, l'equivalent manual és curt i val la pena entendre'l:
function combinar(senyals) {
const controlador = new AbortController();
for (const senyal of senyals) {
if (senyal.aborted) { controlador.abort(senyal.reason); break; }
senyal.addEventListener('abort', () => controlador.abort(senyal.reason), { once: true });
}
return controlador.signal;
}
- Reintents amb retrocés exponencial i jitter
Un 503 durant un desplegament dura segons. Reintentar té sentit… però no de qualsevol manera.
Reintentar immediatament empitjora les coses: si el servidor està saturat, mil clients reintentant alhora l'acaben de rematar. La tècnica correcta s'anomena retrocés exponencial (exponential backoff): esperar cada vegada més entre intents.
intent 1 → falla → espera 300 ms intent 2 → falla → espera 600 ms intent 3 → falla → espera 1200 ms intent 4 → falla → es rendeix
I hi falta un ingredient. Si mil clients fallen alhora i tots esperen exactament 300 ms, tornen tots alhora 300 ms després. Aquell pic sincronitzat s'anomena thundering herd. La solució és el jitter: afegir un component aleatori a l'espera per escampar els reintents.
// js/dades/http.js
const dormir = (ms) => new Promise((resoldre) => setTimeout(resoldre, ms));
/**
* Executa `operacio()` reintentant les fallades reintentables.
* @param {Function} operacio Funció asíncrona SENSE arguments (tanca-la amb un closure)
*/
export async function ambReintents(operacio, {
intents = 3,
baseMs = 300,
maxMs = 5000,
senyal = null
} = {}) {
let ultimError;
for (let intent = 1; intent <= intents; intent += 1) {
try {
return await operacio();
} catch (error) {
ultimError = error;
// No reintentar el que no té remei: 400, 401, 403, 404, cancel·lacions, format
if (!(error instanceof ErrorDeApi) || !error.reintentable) throw error;
if (intent === intents) break;
if (senyal?.aborted) throw error;
// El servidor pot dir quant esperar (429 amb Retry-After); si no, exponencial
const suggerit = Number(error.detall?.retryAfter) * 1000;
const exponencial = Math.min(baseMs * 2 ** (intent - 1), maxMs);
const jitter = Math.random() * exponencial * 0.3; // ±30 % de dispersió
const espera = Number.isFinite(suggerit) && suggerit > 0 ? suggerit : exponencial + jitter;
console.warn(`[nomada] Intent ${intent}/${intents} ha fallat (${error.codi}). Reintent en ${Math.round(espera)} ms.`);
await dormir(espera);
}
}
throw ultimError;
}// Ús: l'operació s'embolcalla en una fletxa per poder repetir-la
const tasques = await ambReintents(
() => demanarJson(construirUrl('/tasques'), { signal: AbortSignal.timeout(5000) }),
{ intents: 3 }
);Fixa't que l'operació és una funció, no una promesa. Una promesa ja llançada no es pot «tornar a llançar»: cal poder crear-ne una de nova a cada intent. És el mateix motiu pel qual ambLimit rebia una promesa (només l'observa) i ambReintents rep una funció (l'executa diverses vegades).
- Idempotència: què es pot reintentar
Abans de reintentar qualsevol cosa, la pregunta decisiva: què passa si la petició sí que va arribar i només es va perdre la resposta?
sequenceDiagram
participant C as Client
participant S as Servidor
C->>S: POST /v1/tasques {"titol":"Revisar premsa"}
S->>S: Crea la tasca 7 ✅
S--xC: La resposta es perd (xarxa caiguda)
Note over C: El client creu que ha fallat
C->>S: POST /v1/tasques {"titol":"Revisar premsa"}
S->>S: Crea la tasca 8 ❌ DUPLICADA!
Per això la idempotència de 07-02 deixa de ser teoria:
| Verb | Idempotent? | Reintentar sense més? |
|---|---|---|
GET |
Sí | Sí, sempre |
HEAD |
Sí | Sí |
PUT /tasques/6 |
Sí (reemplaça amb el mateix contingut) | Sí |
DELETE /tasques/6 |
Sí (esborrar el ja esborrat no canvia res) | Sí, tolerant el 404 del segon intent |
PATCH /tasques/6 {estat:'feta'} |
Depèn: sí si assigna, no si incrementa | Amb compte |
POST /tasques |
No | No, sense clau d'idempotència |
La solució estàndard per poder reintentar un POST és la clau d'idempotència: el client genera un identificador únic de l'operació i l'envia en una capçalera. El servidor recorda les claus ja processades i, si en veu una de repetida, retorna el resultat anterior en lloc de crear un altre recurs.
export async function crearTasca(dades) {
const clau = crypto.randomUUID(); // ← la MATEIXA en tots els reintents
return ambReintents(() => demanarJson(construirUrl('/tasques'), {
method: 'POST',
headers: { ...CAPCALERES_JSON, 'Idempotency-Key': clau },
body: JSON.stringify(dades),
signal: AbortSignal.timeout(10000)
}), { intents: 3 });
}El crucial és que crypto.randomUUID() es crida fora del closure que es reintenta: si fos a dins, cada intent generaria una clau diferent i tornaries al problema dels duplicats. I això només funciona si el servidor implementa la capçalera; si no ho fa, la regla és simple: no reintentis els POST automàticament. Ofereix un botó «Reintentar» i que decideixi la persona.
- Paral·lel sense fragilitat:
allSettled davant d'all
allSettled davant d'allEl panell de la Marta necessita tres coses: les tasques, les hores de l'equip i els avisos. Demanar-les en seqüència suma latències; fer-ho en paral·lel les solapa. Però l'elecció del combinador importa molt, i reprèn directament 05-06.
// ✗ Fràgil: si els avisos fallen, no hi ha tauler
const [tasques, hores, avisos] = await Promise.all([
llistarTasques(), llistarHoresEquip(), llistarAvisos()
]);Promise.all rebutja així que una rebutja. Un 500 a l'endpoint menys important deixa la pantalla en blanc. Per a un panell compost de parts independents, això és un mal repartiment de conseqüències.
// ✓ Robust: cada part es pinta si ha pogut, i les que han fallat es marquen
const resultats = await Promise.allSettled([
llistarTasques(), llistarHoresEquip(), llistarAvisos()
]);
const [tasques, hores, avisos] = resultats.map((r) =>
r.status === 'fulfilled' ? r.value : null
);
if (tasques === null) {
vista.mostrarError("No s'ha pogut carregar el tauler.", { reintentar: () => arrencar() });
} else {
vista.actualitzar({ tauler: new Tauler('Taller Nómada', tasques) });
if (hores === null) vista.marcarSeccioCaiguda('#hores-equip');
if (avisos === null) vista.marcarSeccioCaiguda('#avisos');
}El recordatori dels quatre combinadors, ara amb el criteri de quan fer servir cadascun:
| Combinador | Se salda quan… | Fes-lo servir quan… |
|---|---|---|
Promise.all |
totes compleixen, o una falla | Les parts són indispensables entre si (no pots pintar res sense totes) |
Promise.allSettled |
totes acaben, passi el que passi | Parts independents d'un panell. El cas habitual en interfícies |
Promise.race |
la primera que se salda, compleixi o falli | Timeouts, «el que arribi abans» |
Promise.any |
la primera que compleix | Diversos miralls d'una mateixa dada, val qualsevol |
I un advertiment sobre el paral·lelisme: llançar cinquanta peticions alhora no és més ràpid, és una manera de saturar el servidor i el navegador (que limita les connexions simultànies per origen). Quan en tinguis moltes, trosseja-les en lots.
/** Executa les operacions en lots de `mida`, en lloc de totes alhora. */
export async function perLots(operacions, mida = 5) {
const sortida = [];
for (let i = 0; i < operacions.length; i += mida) {
const lot = operacions.slice(i, i + mida);
sortida.push(...await Promise.allSettled(lot.map((op) => op())));
}
return sortida;
}
- Els quatre estats de la interfície
Una pantalla que parla amb una xarxa mai té dos estats, en té quatre. Programar només «amb dades» i «sense dades» és la causa de les pantalles en blanc eternes i dels «Carregant…» que no s'acaben.
stateDiagram-v2
[*] --> Inicial
Inicial --> Carregant: arrencar()
Carregant --> Exit: arriben dades
Carregant --> Buit: arriben 0 tasques
Carregant --> Error: ErrorDeApi
Error --> Carregant: reintentar()
Exit --> Carregant: refrescar()
Buit --> Carregant: refrescar()
Exit --> Exit: canvi local (optimista)
| Estat | Què es mostra | Què NO fer |
|---|---|---|
| Carregant | Indicador o esquelet de contingut | Pantalla en blanc sense explicació |
| Èxit | Les dades | — |
| Buit | «Encara no hi ha tasques» + acció per crear-ne una | Confondre'l amb un error, o amb «carregant» |
| Error | Què ha passat, en llenguatge planer, + botó Reintentar | «Error: TypeError: Failed to fetch» |
L'estat buit és el que més s'oblida, i el que més desconcerta: una llista buida sense missatge és indistingible d'una llista que no ha carregat. I l'estat error ha de portar sempre una sortida; un missatge sense botó deixa l'usuari amb l'única opció de recarregar a mà.
Implementat amb el que ja tens de 06-06:
// js/vista/tauler-vista.js (ampliació)
const ESTATS_UI = Object.freeze({ INICIAL: 'inicial', CARREGANT: 'carregant',
EXIT: 'exit', BUIT: 'buit', ERROR: 'error' });
export class TaulerVista {
// …el que ja hi havia…
#ui = { fase: ESTATS_UI.INICIAL, error: null };
/** Únic punt de canvi de fase: impossible deixar la pantalla en un estat inconsistent. */
#canviarFase(fase, { error = null } = {}) {
this.#ui = { fase, error };
this.#pintarFase();
}
#pintarFase() {
const { fase, error } = this.#ui;
$('#carregant').hidden = fase !== ESTATS_UI.CARREGANT;
$('#buit').hidden = fase !== ESTATS_UI.BUIT;
$('#error').hidden = fase !== ESTATS_UI.ERROR;
this.#contenidor.hidden = fase !== ESTATS_UI.EXIT;
if (fase === ESTATS_UI.ERROR) {
$('#error-missatge').textContent = error.message; // textContent, no innerHTML (06-02)
$('#error-reintentar').hidden = !error.reintentable;
}
}
async carregarDesDeApi(api) {
this.#canviarFase(ESTATS_UI.CARREGANT);
try {
const tasques = await ambReintents(() => api.llistarTasques(), { intents: 3 });
this.#estat.tauler = new Tauler('Taller Nómada', tasques);
this.#canviarFase(tasques.length === 0 ? ESTATS_UI.BUIT : ESTATS_UI.EXIT);
this.render();
} catch (error) {
if (error.codi === 'cancel·lat') return; // no és una fallada
this.#canviarFase(ESTATS_UI.ERROR, { error });
}
}
}La clau de disseny és #canviarFase: un únic punt pel qual passen totes les transicions. Sense ell, acabes amb sis llocs que posen i treuen hidden i, tard o d'hora, amb l'indicador de càrrega i el missatge d'error visibles alhora.
- Avisos accessibles amb
aria-live
aria-liveA 06-07 vas aprendre que un missatge que només es veu en vermell no existeix per a qui no veu la pantalla. Amb la xarxa, el problema s'agreuja: els canvis passen sols, sense que l'usuari hagi fet res, i un lector de pantalla no els anuncia tret que li ho demanis.
<!-- Estat de càrrega i errors, anunciats automàticament -->
<p id="carregant" class="avis" role="status" aria-live="polite" hidden>Carregant tasques…</p>
<div id="error" class="avis avis--error" role="alert" hidden>
<p id="error-missatge"></p>
<button type="button" id="error-reintentar">Reintentar</button>
</div>
<p id="buit" class="avis" hidden>Encara no hi ha tasques. <button type="button" id="crear-primera">Crear la primera</button></p>| Atribut | Quan anuncia | Per a què |
|---|---|---|
aria-live="polite" |
En acabar el que el lector estigui dient | Canvis d'estat, «Carregant», «3 tasques» |
aria-live="assertive" |
Interromp immediatament | Només urgències reals |
role="status" |
Equival a polite |
Missatges informatius |
role="alert" |
Equival a assertive |
Errors que exigeixen atenció |
Quatre regles pràctiques:
- El contenidor ha d'existir al DOM abans de ficar-hi text. Si el crees i li poses el missatge en el mateix instant, molts lectors no l'anuncien. D'aquí el
hiddenen lloc de crear i destruir. - No abusis d'
assertive. Interrompre a mitja frase és agressiu; reserva'l per a errors. - Esmorteeix els avisos repetitius. Un
aria-liveque canvia a cada tecla converteix el lector en una metralladora: és el mateix argument que justificava eldebouncea 06-07. - Mou el focus al botó «Reintentar» quan aparegui un error després d'una acció de l'usuari. Un missatge anunciat però inabastable amb el teclat serveix de poc.
- Optimistic UI: aplicar abans de confirmar
La Marta marca una tasca com a feta. Si esperes la resposta del servidor per moure la targeta, la interfície es percep lenta encara que trigui 200 ms. La tècnica optimista consisteix a aplicar el canvi immediatament, enviar la petició en segon pla i revertir si falla.
sequenceDiagram
participant M as Marta
participant V as Vista
participant A as API
M->>V: clic a "marcar feta"
V->>V: 1 · desa còpia de l'estat anterior
V->>V: 2 · aplica el canvi i repinta ⚡ (instantani)
V->>A: 3 · PATCH /tasques/6 {"estat":"feta"}
alt Èxit
A-->>V: 200 OK
V->>V: confirma (treu la marca de "pendent de sincronitzar")
else Fallada
A-->>V: 500 / xarxa caiguda
V->>V: 4 · REVERTEIX a l'estat desat
V->>M: «No s'ha pogut desar. Reintentar»
end
Aquí és on rendeixen les actualitzacions immutables de 04-07: com que no vas mutar l'estat anterior, revertir és senzillament tornar-lo a fer servir.
// js/vista/controlador.js
async function canviarEstatOptimista(id, estatNou) {
const anterior = tauler.cercarPerId(id).toJSON(); // 1 · foto de l'estat previ
tauler.canviarEstat(id, estatNou); // 2 · canvi local immediat
vista.render();
vista.marcarPendent(id, true); // senyal visual subtil de "sense confirmar"
try {
await ambReintents(
() => api.actualitzarTasca(id, { estat: estatNou }),
{ intents: 2 }
);
vista.marcarPendent(id, false); // 3 · confirmat
repositori.desar(tauler);
} catch (error) {
tauler.substituir(Tasca.desDeJSON(anterior)); // 4 · revertir
vista.render();
vista.avisar(`No s'ha pogut desar «${anterior.titol}». ${error.message}`, {
accio: { text: 'Reintentar', enPremer: () => canviarEstatOptimista(id, estatNou) }
});
}
}Quan fer-la servir i quan no:
| Fes servir optimisme quan… | Evita'l quan… |
|---|---|
| El canvi gairebé sempre té èxit | El servidor el pot rebutjar sovint |
| És fàcil de revertir (un estat, un text) | Hi ha efectes irreversibles (cobraments, correus enviats) |
| L'usuari percep la latència | L'operació triga igualment (pujar un fitxer) |
| La dada és del propi usuari | Altres poden haver-la canviat alhora |
I una regla d'honestedat: si apliques un canvi optimista, marca'l. Un punt, una opacitat lleugera, una icona de «sincronitzant». Que l'usuari tanqui el portàtil creient que alguna cosa s'ha desat quan no ha estat així és pitjor que fer-lo esperar 200 ms.
- Nómada Tasques:
js/dades/http.js i la vista
js/dades/http.js i la vistaTotes les peces juntes. El mòdul http.js queda com a capa genèrica reutilitzable, i api-tasques.js s'hi recolza:
// js/dades/http.js — resum del que exporta
export { demanarJson }; // fetch + classificació d'errors en ErrorDeApi
export { ambReintents }; // retrocés exponencial amb jitter, només per a errors reintentables
export { senyalAmb }; // AbortSignal.any([timeout, externa])
export { perLots }; // paral·lelisme limitat amb allSettled// js/dades/api-tasques.js — reescrit sobre http.js
import { demanarJson, ambReintents, senyalAmb } from './http.js';
import { Tasca } from '../model/tasca.js';
const TEMPS = Object.freeze({ lectura: 5000, escriptura: 10000 });
export function crearApiTasques({ base = 'http://localhost:3000', signal = null } = {}) {
const url = (ruta, params = {}) => {
const u = new URL(base + ruta);
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== '') u.searchParams.set(k, v);
}
return u;
};
return {
/** Lectura: idempotent, es reintenta sense por. */
async llistarTasques(filtres = {}) {
const planes = await ambReintents(
() => demanarJson(url('/tasques', filtres), { signal: senyalAmb(TEMPS.lectura, signal) }),
{ intents: 3, senyal: signal }
);
return planes.map((d) => Tasca.desDeJSON(d));
},
/** Creació: NO idempotent. Un sol intent + clau, i que l'usuari decideixi si reintenta. */
async crearTasca(dades) {
const plana = await demanarJson(url('/tasques'), {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify(dades),
signal: senyalAmb(TEMPS.escriptura, signal)
});
return Tasca.desDeJSON(plana);
},
/** PATCH que assigna un valor fix: idempotent, reintentable. */
async actualitzarTasca(id, canvis) {
const plana = await ambReintents(
() => demanarJson(url(`/tasques/${id}`), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(canvis),
signal: senyalAmb(TEMPS.escriptura, signal)
}),
{ intents: 2 }
);
return Tasca.desDeJSON(plana);
},
/** DELETE: idempotent. Un 404 al segon intent significa que el primer va funcionar. */
async esborrarTasca(id) {
try {
await ambReintents(
() => demanarJson(url(`/tasques/${id}`), { method: 'DELETE', signal: senyalAmb(TEMPS.escriptura, signal) }),
{ intents: 2 }
);
} catch (error) {
if (error.status !== 404) throw error; // ja estava esborrada: és el resultat desitjat
}
return true;
}
};
}I l'arrencada completa de l'aplicació, amb local primer, xarxa després, i tots els estats coberts:
// js/app.js
const controladorApp = new AbortController();
const api = crearApiTasques({ base: 'http://localhost:3000', signal: controladorApp.signal });
const repositori = new RepositoriLocal();
async function arrencar() {
// 1 · El que és local es pinta a l'instant: mai una pantalla en blanc si hi ha dades
const local = repositori.carregar();
if (local !== null) { vista.actualitzar({ tauler: local }); }
// 2 · La veritat del servidor, amb tots els estats gestionats
await vista.carregarDesDeApi(api);
repositori.desar(vista.tauler);
}
$('#error-reintentar').addEventListener('click', arrencar, { signal: controladorApp.signal });
window.addEventListener('online', () => arrencar(), { signal: controladorApp.signal });
window.addEventListener('offline', () => vista.avisar('Sense connexió. Els canvis es desen en local.'),
{ signal: controladorApp.signal });
arrencar();Aquell únic controladorApp cancel·la de cop les peticions en vol i tots els oients quan l'aplicació es desmunti. Un AbortController per a tota la vida de l'aplicació, i altres d'efímers per a operacions concretes com la cerca: aquest és el repartiment habitual.
Errors Habituals i Consells
- Tractar
AbortErrorcom una fallada. Mostrar «Error en carregar» quan ets tu qui va cancel·lar és un bug clàssic. Filtra sempre pererror.name === 'AbortError'o, ambdemanarJson, percodi === 'cancel·lat'. - Reutilitzar un
AbortControllerja avortat. El seu senyal queda avortat per sempre i la petició següent mor a l'instant. Crea'n un de nou per operació. - Reintentar un
POSTsense clau d'idempotència. Crea duplicats invisibles: l'usuari veu una tasca, la base de dades en té tres. - Reintentar un
400o un403. No funcionarà mai; només afegeix latència i soroll als registres. - Reintentar sense jitter. Sincronitza tots els clients i colpeja el servidor en onades justament quan pitjor està.
- Confiar en
Promise.racecom si cancel·lés. No cancel·la: només ignora el perdedor, que continua consumint recursos. - No posar cap timeout. «Carregant…» etern.
fetchno ho fa per tu. - Fer servir
Promise.allper a un panell de parts independents. Una fallada secundària deixa la pantalla buida.allSettled. - Oblidar l'estat buit. Una llista sense elements i sense missatge sembla trencada.
- Mostrar el missatge tècnic a l'usuari.
TypeError: Failed to fetchno significa res per a la Marta. Tradueix, i desa el detall tècnic a la consola. - Aplicar un canvi optimista sense poder revertir-lo. Si vas mutar l'estat anterior, no hi ha marxa enrere. Les actualitzacions immutables de 04-07 són el requisit, no un adorn.
- Consell: prova les fallades a propòsit. A DevTools → Network pots simular Offline i Slow 3G, i
https://httpstat.us/503et retorna el codi que vulguis. Una gestió d'errors que mai s'ha executat no està provada. - Consell: registra l'error tècnic complet, mostra l'humà.
console.error(error.descriure(), error.causa)a la consola; a la pantalla, la frase curta. - Consell: un timeout per tipus d'operació. Llegir i escriure no mereixen el mateix marge.
- Consell: no reintentis en bucle infinit. Tres intents i una sortida clara. La insistència eterna esgota bateria i paciència.
Exercicis
Exercici 1 — Reintents amb Retry-After.
Amplia ambReintents perquè, quan l'error sigui un 429, llegeixi la capçalera Retry-After de la resposta i esperi exactament aquell temps en lloc d'aplicar el retrocés exponencial. Necessitaràs que demanarJson desi aquella capçalera a l'ErrorDeApi. La capçalera pot venir en segons (Retry-After: 30) o com a data HTTP (Retry-After: Wed, 20 Sep 2026 10:00:00 GMT): resol els dos formats i limita l'espera a un màxim de 60 segons.
Exercici 2 — Cercador cancel·lable amb estats.
Escriu crearCercador({ camp, enCanviar, enError, retard = 300 }) que retorni una funció cercar(text) i una funció cancellar(). Ha d'esmorteir amb debounce, cancel·lar la petició anterior a cada cerca nova, ignorar els AbortError, imposar un timeout de 4 segons i cridar enCanviar amb { fase: 'carregant' | 'exit' | 'buit' | 'error', tasques, error }. Que un text buit cancel·li el que hi hagués en vol i retorni la fase 'inicial'.
Exercici 3 — Canvi d'estat optimista amb reversió.
Escriu crearAccioOptimista({ tauler, vista, api, repositori }) que retorni canviarEstat(id, estatNou) amb el cicle complet: foto de l'estat previ amb toJSON(), canvi local, render, marca de pendent, petició amb dos reintents, i en cas de fallada reversió amb Tasca.desDeJSON, render i avís amb acció de reintent. Afegeix una salvaguarda: si ja hi ha una operació en vol per a aquella mateixa tasca, ignora la nova pulsació en lloc d'encadenar dos canvis optimistes.
Solucions
Solució 1
// A demanarJson, en construir l'error de resposta:
throw new ErrorDeApi(missatge, {
status: resposta.status,
codi,
url: String(url),
detall,
reintentarDespres: resposta.headers.get('retry-after') // ← es desa tal com ha arribat
});/** Converteix la capçalera Retry-After (segons o data HTTP) en mil·lisegons d'espera. */
function esperaSuggerida(capcalera, maxMs = 60000) {
if (!capcalera) return null;
const segons = Number(capcalera);
if (Number.isFinite(segons)) return Math.min(segons * 1000, maxMs);
const data = Date.parse(capcalera); // format data HTTP
if (Number.isNaN(data)) return null;
return Math.min(Math.max(data - Date.now(), 0), maxMs);
}// Dins del catch d'ambReintents, substituint el càlcul de l'espera:
const suggerida = esperaSuggerida(error.reintentarDespres);
const exponencial = Math.min(baseMs * 2 ** (intent - 1), maxMs);
const espera = suggerida ?? (exponencial + Math.random() * exponencial * 0.3);El Number(capcalera) distingeix els dos formats sense expressions regulars: Number('30') dona 30, mentre que Number('Wed, 20 Sep…') dona NaN i cau al Date.parse. I el sostre de 60 s protegeix d'un servidor que demani esperar una hora: passat cert punt, val més rendir-se i deixar que l'usuari decideixi.
Solució 2
import { debounce } from '../util/temps.js';
import { demanarJson } from './http.js';
import { Tasca } from '../model/tasca.js';
export function crearCercador({ base, enCanviar, retard = 300 }) {
let controlador = null;
function cancellar() {
controlador?.abort();
controlador = null;
}
const llancar = debounce(async (text) => {
cancellar(); // 1 · mata l'anterior
controlador = new AbortController();
enCanviar({ fase: 'carregant', tasques: [], error: null });
try {
const url = new URL(`${base}/tasques`);
url.searchParams.set('q', text);
const planes = await demanarJson(url, {
signal: AbortSignal.any([controlador.signal, AbortSignal.timeout(4000)])
});
const tasques = planes.map((d) => Tasca.desDeJSON(d));
enCanviar({ fase: tasques.length === 0 ? 'buit' : 'exit', tasques, error: null });
} catch (error) {
if (error.codi === 'cancel·lat') return; // 2 · la substitueix una cerca més nova
enCanviar({ fase: 'error', tasques: [], error });
}
}, retard);
function cercar(text) {
const net = text.trim();
if (net === '') {
cancellar();
enCanviar({ fase: 'inicial', tasques: [], error: null });
return;
}
llancar(net);
}
return { cercar, cancellar };
}Fixa't en l'ordre dins de llancar: primer es cancel·la l'anterior, després es crea el controlador nou i només llavors s'anuncia «carregant». A l'inrevés, la cancel·lació de l'anterior podria arribar a sobreescriure la fase que acabes de posar. I AbortSignal.any combina els dos motius per cancel·lar —una cerca més nova, o el timeout— en un sol senyal.
Solució 3
export function crearAccioOptimista({ tauler, vista, api, repositori }) {
const enVol = new Set(); // ids amb una operació pendent
async function canviarEstat(id, estatNou) {
if (enVol.has(id)) return false; // salvaguarda: una alhora per tasca
enVol.add(id);
const anterior = tauler.cercarPerId(id).toJSON(); // foto immutable de l'estat previ
try {
tauler.canviarEstat(id, estatNou); // pot llançar ErrorDeValidacio per la R6
} catch (error) {
enVol.delete(id);
vista.avisar(error.message);
return false;
}
vista.render();
vista.marcarPendent(id, true);
try {
await ambReintents(() => api.actualitzarTasca(id, { estat: estatNou }), { intents: 2 });
vista.marcarPendent(id, false);
repositori.desar(tauler);
return true;
} catch (error) {
if (error.codi === 'cancel·lat') return false;
tauler.substituir(Tasca.desDeJSON(anterior)); // reversió
vista.render();
vista.avisar(`No s'ha pogut desar «${anterior.titol}».`, {
accio: { text: 'Reintentar', enPremer: () => canviarEstat(id, estatNou) }
});
return false;
} finally {
enVol.delete(id); // passi el que passi, s'allibera
}
}
return { canviarEstat, hiHaPendents: () => enVol.size > 0 };
}Tres detalls importants. El Set d'ids en vol evita que dues pulsacions ràpides encadenin dos canvis optimistes amb reversions creuades —un origen habitual d'estats impossibles—. El finally allibera l'id sempre, inclosos els camins d'error i el return primerenc. I la validació local (canviarEstat del tauler, que aplica la regla R6) es fa abans de pintar res: no té sentit ser optimista amb un canvi que el teu propi model rebutja.
Conclusió
Aquesta lliçó és la frontera entre un exercici i un producte. Tens el mapa complet de les set fallades d'una petició —xarxa, DNS, CORS, 4xx, 5xx, resposta no-JSON i resposta lenta— i, per a cadascuna, com detectar-la, si mereix reintent i què explicar a l'usuari; amb les dues idees que governen tota la resta: només les fallades de transport fan rebutjar fetch, i la resposta lenta no es detecta sola, l'has de provocar tu amb un timeout. Aquell coneixement viu encapsulat a ErrorDeApi extends Error, amb el seu status, el seu codi i els seus getters reintentable i requereixSessio, exactament el mateix patró que et va donar ErrorDeValidacio amb el seu camp camp a 05-02.
Saps escriure demanarJson, que tradueix qualsevol fallada a un ErrorDeApi classificat, comprova el content-type per caçar l'HTML disfressat de JSON, respecta el 204 sense cos i clona la resposta abans de llegir el detall de l'error. Domines AbortController: el parell controlador/senyal, aborted i reason, l'ús d'un sol senyal per cancel·lar peticions i oients alhora, la regla que un controlador és d'un sol ús, i l'obligació de distingir AbortError d'un error real —cancel·lar a propòsit no és fallar—. Ho has aplicat al cas que ho justifica: el cercador on debounce redueix les peticions i abort garanteix que només l'última pinti, tancant la race condition de les respostes que tornen desordenades.
Saps imposar timeouts i per què AbortSignal.timeout() és superior a Promise.race —el primer talla la petició, el segon només ignora el perdedor mentre continua consumint la connexió—, i combinar motius de cancel·lació amb AbortSignal.any(). Saps reintentar amb retrocés exponencial i jitter, entens per què existeix el jitter (el thundering herd) i, sobretot, saps què es pot reintentar: GET, PUT i DELETE sense problema, POST només amb clau d'idempotència generada fora del closure que es repeteix. Saps triar combinador per al paral·lelisme, amb Promise.allSettled com a opció per defecte en interfícies perquè una fallada secundària no ha de buidar la pantalla, i trossejar en lots quan les peticions són moltes.
I saps que una pantalla connectada a una xarxa té quatre estats, no dos: carregant, èxit, buit i error amb reintent; que les transicions han de passar per un únic punt per no deixar la interfície en estats impossibles; que els avisos necessiten aria-live, role="status" i role="alert" per existir també per a qui no veu la pantalla; i que l'optimistic UI —aplicar el canvi, enviar, revertir si falla— només és possible perquè les actualitzacions immutables de 04-07 conserven intacte l'estat anterior, i només és honest si marques visualment el que encara no està confirmat.
Amb js/dades/http.js i js/dades/api-tasques.js reescrits, Nómada Tasques ja és una aplicació connectada que aguanta el món real. Però li queda un límit conceptual, no tècnic: el navegador només s'assabenta de les coses quan pregunta. Si l'Iván mou una tasca des del seu portàtil, el tauler de la Marta continuarà mostrant l'estat antic fins que ella recarregui o premi refrescar. Podries preguntar cada cinc segons, però això és gastar bateria i amplada de banda perquè gairebé sempre la resposta sigui «res de nou». El que cal és que el servidor pugui parlar primer: una connexió permanent i bidireccional per la qual els canvis arribin sols, en l'instant en què passen. Això és WebSockets, on el tauler del Taller Nómada deixarà de ser una còpia per persona per convertir-se en un de sol, compartit i viu.
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
