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.

55 min Debugging

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:

  1. Tre Promise risolvono, una rigetta. Usa Promise.all. Cosa succede?
  2. Stesso scenario con Promise.allSettled. Differenza?
  3. Aggiungi un window.addEventListener('unhandledrejection', ...). Riproduci una rejection non catturata. Funziona?
  4. Crea una promise che non chiama mai né resolve né reject. Aspettala con await. Confronta con withTimeout.

Errori frequenti del principiante

  • Saltare .catch o try/catch. Errori invisibili.
  • async senza await in un altro async. Hai una promise che restituisci ma nessuno aspetta.
  • Promise.all per cose indipendenti che possono fallire isolatamente. Usa allSettled.
  • Ignorare unhandledrejection. Il primo segnale che hai bug invisibili in produzione.