Menu

Gestione degli errori async in JavaScript: try/catch e Promise

Come scorrono davvero gli errori nel JavaScript asincrono: try/catch con async/await, .catch sulle promise e le trappole che ingoiano i fallimenti in silenzio.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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 await rischioso è dentro un try/catch, oppure la promise restituita viene gestita da chi chiama con .catch()?
  • Sto davvero facendo await sulla chiamata, o l'ho lanciata e dimenticata per sbaglio?
  • Nel caso di fetch, controllo res.ok prima di fidarmi della risposta?
  • Quando eseguo operazioni in parallelo, lo strumento giusto è Promise.all o mi serve Promise.allSettled?
  • C'è un .catch() di primo livello o un gestore unhandledrejection, 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}.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA