Gli errori nel codice async non funzionano come quelli sincroni
Nel JavaScript sincrono, un errore lanciato risale lo stack delle chiamate finché un try/catch non lo cattura, altrimenti il programma va in crash. Il codice asincrono rompe questo modello. Quando una richiesta di rete fallisce, la funzione che l'ha avviata ha già restituito il controllo da un pezzo. Non c'è più uno stack da risalire.
Le promise risolvono il problema dando agli errori un canale tutto loro. Una promise può essere soddisfatta con un valore oppure rifiutata con un motivo. La rejection è l'equivalente asincrono di lanciare un errore. Tutto ciò che trovi in questa pagina serve a far atterrare le rejection in un punto che controlli tu, invece di vederle sparire.
Il try/catch viene eseguito ed esce senza problemi. La rejection avviene 50ms dopo, quando il blocco try è finito da tempo. Niente la cattura. Ecco la trappola.
try/catch torna a funzionare con await
Nel momento in cui fai await su una promise, una rejection diventa un errore lanciato dentro la funzione async. Un try/catch attorno la cattura come qualsiasi throw sincrono:
Questo è il pattern da usare per primo. await riporta il mondo asincrono alla forma familiare del try/catch. Metti dentro il try le chiamate con await che possono fallire e gestiscile nel catch.
Un dettaglio da tenere a mente: è coperta solo la chiamata su cui fai await. Se avvii una promise senza aspettarla, gli errori sfuggono comunque.
Il bug più comune: dimenticare await
Se chiami una funzione async senza await (o senza restituire la sua promise), le rejection scivolano oltre il try/catch che la circonda:
Il blocco try termina senza errori. La rejection avviene al tick successivo, senza nessuno che la catturi. Nella console vedrai un avviso di "unhandled promise rejection".
La soluzione è sempre la stessa: fai await sulla chiamata, oppure return della promise, così chi chiama la funzione può aspettarla.
.catch() è l'altra faccia della stessa medaglia
Puoi gestire le rejection senza async/await concatenando .catch():
.catch(fn) è una scorciatoia per .then(undefined, fn). Gestisce qualsiasi rejection avvenuta prima nella catena. Un .catch() alla fine di una catena è l'equivalente asincrono di un try/catch di primo livello: l'ultima linea di difesa prima che la rejection diventi "non gestita".
Mescolare i due stili va benissimo. Un pattern comune è usare async/await dentro una funzione e lasciare che chi la chiama aggiunga .catch():
fetch non rifiuta sugli errori HTTP
Questa frega tutti almeno una volta. fetch rifiuta solo in caso di problemi a livello di rete: lookup DNS fallito, connessione rifiutata, richiesta annullata. Una risposta 404 o 500 è considerata un fetch riuscito. La promise si risolve; solo che si risolve con una risposta il cui ok vale false.
Se vuoi che gli errori HTTP finiscano nel blocco catch, controlla res.ok e lancia l'errore in modo esplicito:
È codice ripetitivo che conviene spostare in un helper appena ti accorgi di averlo scritto due volte.
Promise.all fallisce subito, Promise.allSettled no
Promise.all prende un array di promise e si risolve con un array di risultati, a meno che una non venga rifiutata: in quel caso rifiuta immediatamente con quell'errore. Le altre promise continuano a girare, ma i loro risultati vengono buttati via.
Fallire subito è il comportamento giusto quando ti servono tutti i risultati e un solo errore rende inutile l'intera operazione. Quando invece vuoi ogni esito a prescindere, del tipo "prova questi cinque upload e dimmi quali sono riusciti e quali no", usa Promise.allSettled:
allSettled non viene mai rifiutata. Ogni voce è {status: "fulfilled", value} oppure {status: "rejected", reason}.
Rilanciare gli errori e catch mirati
Non tutti gli errori vanno nello stesso gestore. Un pattern comune è catturare, esaminare e rilanciare tutto ciò che non ti aspettavi:
Ingoiare ogni errore con un catch (err) {} vuoto nasconde bug veri. Cattura ciò che puoi gestire in modo sensato e rilancia il resto.
Le unhandled rejection sono la tua rete di sicurezza
Anche con codice scritto con cura, prima o poi qualcosa sfugge. Sia Node.js sia i browser offrono un hook globale per le rejection che nessuno ha catturato:
// Browser
window.addEventListener("unhandledrejection", event => {
console.error("non gestita:", event.reason);
event.preventDefault(); // sopprime l'avviso predefinito nella console
});
// Node.js
process.on("unhandledRejection", reason => {
console.error("non gestita:", reason);
});
Non sostituisce una gestione corretta: è un hook di ultima istanza per log o telemetria. Nel Node.js moderno, una unhandled rejection per impostazione predefinita manda in crash il processo, ed è di solito quello che vuoi in produzione. Registra l'errore, poi lascia che il processo muoia e riparta pulito.
Una checklist pratica
Quando una funzione async tocca qualcosa che può fallire, chiediti:
- Ogni
awaitrischioso è dentro untry/catch, oppure la promise restituita viene gestita da chi chiama con.catch()? - Sto davvero facendo
awaitsulla chiamata, o l'ho lanciata e dimenticata per sbaglio? - Nel caso di
fetch, controllores.okprima di fidarmi della risposta? - Quando eseguo operazioni in parallelo, lo strumento giusto è
Promise.allo mi servePromise.allSettled? - C'è un
.catch()di primo livello o un gestoreunhandledrejection, così che niente sparisca in silenzio?
Fai bene questi cinque punti e il tuo codice async smetterà di sorprenderti con errori che svaniscono nell'event loop.
Prossimo passo: gli ES Modules
La gestione degli errori async chiude il capitolo sull'asincronia. Ora passiamo a come si organizza il codice JavaScript su più file: import, export e il sistema di moduli su cui si regge ogni progetto moderno.
Domande frequenti
Come si gestiscono gli errori in una funzione async?
Racchiudi le chiamate con await in un blocco try/catch. Qualsiasi rejection di una promise attesa diventa un errore lanciato che il catch riceve. Puoi anche lasciare che l'errore si propaghi e gestirlo nel punto in cui chiami la funzione, con .catch() sulla promise che restituisce.
Perché il mio try/catch non cattura l'errore?
Di solito perché l'errore avviene in codice che non stai aspettando con await. Se chiami una funzione async senza await (e senza restituire la sua promise), qualsiasi rejection sfugge al try/catch che la circonda. Fai sempre await o return della promise di cui vuoi intercettare gli errori.
Cosa succede se una promise viene rifiutata e nessuno la cattura?
Ottieni una unhandled rejection. Node.js emette l'evento unhandledRejection e, nelle versioni recenti, per impostazione predefinita termina il processo. I browser emettono window.onunhandledrejection e mostrano un avviso nella console. In ogni caso, aggiungi un .catch() oppure gestiscila con un try/catch attorno a un await.
Come gestisce gli errori Promise.all?
Promise.all viene rifiutata non appena una qualsiasi delle promise in ingresso fallisce: le altre continuano a girare, ma i loro risultati vengono scartati. Se vuoi ogni esito a prescindere dai fallimenti, usa Promise.allSettled, che si risolve con un array di voci {status, value} o {status, reason}.