modulo 07 / lezione 36
Debugging asincrono e interfacce dinamiche
Promise, async/await ed errori silenziosi
Riconoscere promise non attese, catch mancanti, stati pendenti, unhandled rejection e fallimenti che non arrivano in console.
In parole semplici
Le Promise risolvono un grande problema (callback hell) e ne aggiungono uno nuovo: gli errori silenziosi. Una Promise non attesa, un catch dimenticato, una promise pendente: tutti producono bug che non gridano. Questa lezione ti insegna a far gridare ciò che tace.
Parole nuove
- Promise: oggetto che rappresenta un valore futuro.
- stato: pending, fulfilled, rejected.
- catch: gestore dell’errore della promise.
- unhandled rejection: rejection senza catch.
- async / await: zucchero sintattico per usare Promise come fossero sincrone.
Cosa impari
- Riconoscere i tre stati di una Promise.
- Catturare unhandled rejection lato browser e Node.
- Diagnosticare promise pendenti che non si risolvono mai.
Tre stati
pending (in attesa)
├── ✓ fulfilled (valore)
└── ✗ rejected (errore)
Una promise transita una sola volta: da pending a fulfilled o rejected. Mai indietro.
Bug 1: catch dimenticato
fetch("/api/utenti").then(r => r.json()).then(setUtenti);
// se /api/utenti fallisce, nessuno gestisce l'errore
Sintomi:
- Console mostra
Uncaught (in promise) TypeError. - L’interfaccia resta in loading senza errore visibile.
- Eventualmente:
window.onunhandledrejection.
Fix:
fetch("/api/utenti")
.then(r => r.json())
.then(setUtenti)
.catch(err => mostraErrore(err));
Bug 2: errore dentro async senza try/catch
async function caricaUtenti() {
const r = await fetch("/api/utenti"); // può lanciare
const data = await r.json();
setUtenti(data);
}
caricaUtenti(); // dimenticato il .catch
async/await lancia eccezioni come codice sincrono, ma la chiamante deve await o .catch la promise risultante.
Fix:
async function caricaUtenti() {
try {
const r = await fetch("/api/utenti");
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
setUtenti(data);
} catch (err) {
mostraErrore(err);
}
}
caricaUtenti();
Bug 3: promise non risolta
const promessa = new Promise((resolve, reject) => {
if (qualcosa) resolve();
// dimenticato di chiamare reject altrove
});
await promessa;
// blocco eterno
Sintomi: await non torna mai. UI bloccata in loading. Nessun errore.
Fix: ogni new Promise deve avere tutti i rami che chiamano resolve o reject. Se hai un timeout, lancialo:
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), ms)
),
]);
}
await withTimeout(fetch("/api/utenti"), 5000);
Bug 4: fire-and-forget
function salvaInBackground(dato) {
fetch("/api/audit", { method: "POST", body: JSON.stringify(dato) });
// promise restituita ma non gestita
}
Sintomi: errori invisibili. Se il server è giù, nessuno saprà mai.
Fix:
function salvaInBackground(dato) {
fetch("/api/audit", { method: "POST", body: JSON.stringify(dato) })
.catch(err => logger.warn("audit fallito", err));
}
Oppure dichiara esplicitamente “fire-and-forget”:
void salvaInBackground(dato); // intenzionale
Catturare unhandled rejection
Browser
window.addEventListener("unhandledrejection", (event) => {
console.error("Rejection non gestita:", event.reason);
// qui puoi mandare al sistema di monitoring
event.preventDefault(); // impedisce stampa in console (opzionale)
});
Node.js
process.on("unhandledRejection", (reason) => {
console.error("Rejection non gestita:", reason);
// in Node 15+ il default è terminare il processo
});
Aggiungerli all’inizio dell’app, prima del codice applicativo.
Promise.all vs Promise.allSettled
Promise.all fallisce alla prima rejection:
const [a, b, c] = await Promise.all([
fetch("/api/a"),
fetch("/api/b"),
fetch("/api/c"),
]);
// se /api/b fallisce, l'intera Promise.all rigetta
Promise.allSettled aspetta tutte e restituisce gli esiti:
const results = await Promise.allSettled([...]);
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.error(r.reason);
});
Per debug, allSettled permette di vedere quale delle promise è fallita.
Promise.any
Restituisce la prima fulfilled, oppure AggregateError se tutte falliscono:
const r = await Promise.any([apiPrincipale(), apiFallback()]);
Utile per fallback. Cattura l’AggregateError per vedere tutte le cause.
Debug con DevTools
In Chrome: tab Sources → “Pause on exceptions” + “Pause on caught exceptions” (modulo 3.2). Cattura anche le rejection non catturate.
In Node: node --inspect-brk + DevTools attaccato. Stesse opzioni.
Esercizio guidato
Crea un piccolo programma con quattro chiamate fetch fittizie (usa setTimeout con reject per simulare errori). Per ogni scenario:
- Tre Promise risolvono, una rigetta. Usa
Promise.all. Cosa succede? - Stesso scenario con
Promise.allSettled. Differenza? - Aggiungi un
window.addEventListener('unhandledrejection', ...). Riproduci una rejection non catturata. Funziona? - Crea una promise che non chiama mai né resolve né reject. Aspettala con
await. Confronta conwithTimeout.
Errori frequenti del principiante
- Saltare
.catchotry/catch. Errori invisibili. asyncsenzaawaitin un altroasync. Hai una promise che restituisci ma nessuno aspetta.Promise.allper cose indipendenti che possono fallire isolatamente. UsaallSettled.- Ignorare
unhandledrejection. Il primo segnale che hai bug invisibili in produzione.