Illustrazione di error handling in JavaScript con try…catch e async/await, con finestra di codice e simboli di errore e successo

Async/await in JavaScript: guida pratica con try/catch e gestione errori

Async e await ti permettono di lavorare con codice asincrono senza impazzire tra then annidati. In questo articolo vedrai esempi chiari per evitare callback hell e bug nascosti.

Scrivere codice asincrono può sembrare complesso, ma con la sintassi async await JavaScript diventa un processo intuitivo e leggibile. In questa guida vediamo come funzionano le due parole chiave, come si confrontano con le promesse e come gestire gli errori in modo robusto con try...catch, finally e il controllo delle risposte di fetch.

Indice

Il problema: la complessità asincrona

In passato, la gestione delle operazioni asincrone in JavaScript portava spesso alla “Callback Hell”, una piramide di funzioni annidate difficile da leggere e manutenere. Le Promise hanno migliorato la situazione, introducendo una catena di .then() più gestibile, con un .catch() in fondo per intercettare gli errori.

Tuttavia, anche le promesse possono diventare verbose con catene complesse. La vera rivoluzione è arrivata con async/await, uno “zucchero sintattico” costruito sopra le promesse che ci permette di scrivere codice asincrono che sembra e si comporta come codice sincrono.

Cosa sono async e await

Per capire a fondo il loro funzionamento, analizziamoli separatamente. Sono due facce della stessa medaglia e lavorano sempre in coppia.

  • async: quando anteposta a una funzione, la parola chiave async assicura che la funzione restituisca sempre una promessa. Se la funzione restituisce un valore, JavaScript lo avvolge automaticamente in una promessa risolta con quel valore.
  • await: questa parola chiave può essere usata solo all’interno di una funzione async (o al livello più alto di un modulo ES). Mette in pausa l’esecuzione della funzione fino a quando la promessa non viene risolta (completata o rifiutata) e restituisce il suo risultato.

async function saluta() {
  return "Ciao Mondo!"; // JavaScript restituisce Promise.resolve("Ciao Mondo!")
}

saluta().then(console.log); // Stampa: Ciao Mondo!
        

Async/await in azione: prima e dopo

Il modo migliore per apprezzare la differenza è confrontare lo stesso codice scritto prima con le promesse pure e poi con async/await. Prendiamo un esempio comune: recuperare dati da un’API.

Prima: le promesse con .then()

Con le promesse, usiamo .then() per concatenare le operazioni asincrone. Il codice è funzionale, ma la logica è distribuita su più blocchi.


function fetchDataWithPromise() {
  fetch('https://api.example.com/data')
    .then(response => {
      if (!response.ok) {
        throw new Error('Errore di rete!');
      }
      return response.json();
    })
    .then(data => {
      console.log(data);
    })
    .catch(error => {
      console.error('Si è verificato un problema:', error);
    });
}
        

Dopo: la chiarezza di async/await

Ora riscriviamo la stessa logica. Il flusso diventa lineare, quasi come se stessimo leggendo un semplice script sincrono. La gestione degli errori è centralizzata in un blocco try...catch.


async function fetchDataWithAsyncAwait() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Errore di rete!');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Si è verificato un problema:', error);
  }
}
        

Gestire gli errori con try, catch e finally

Se c’è una certezza nello sviluppo software, è questa: qualcosa andrà storto. Un’API che non risponde, un JSON malformato o una connessione internet che cade proprio mentre l’utente preme “Invia”. Se non gestisci questi casi, la tua applicazione può bloccarsi lasciando l’utente davanti a una schermata bianca.

Uno dei vantaggi più significativi di async/await è la possibilità di usare i blocchi try...catch standard di JavaScript. Questo approccio è spesso più familiare e intuitivo rispetto al metodo .catch() delle promesse e rende più chiaro “dove può succedere l’errore” e “cosa facciamo quando succede”.

La sintassi è semplice: “proviamo” (try) a eseguire un blocco di codice; se qualcosa va storto, lo “catturiamo” (catch). Il blocco finally è opzionale ma utilissimo: viene eseguito sempre, sia in caso di successo sia in caso di errore.

Esempio rapido: chiamata API + parsing JSON, con gestione errori e cleanup.

Analisi del flusso

  1. Try: se una riga lancia un errore (o un await riceve una Promise rigettata), l’esecuzione salta subito al catch più vicino. La gestione degli errori asincroni diventa così identica a quella degli errori sincroni.
  2. Catch: riceve l’errore. Qui gestisci il fallimento (messaggio utente, fallback, retry, logging).
  3. Finally: pulizia sempre garantita (nascondere spinner, chiudere connessioni, ripristinare stato UI).

Errori HTTP con fetch: la trappola più comune

Un errore comune è pensare che catch intercetti qualsiasi problema con fetch. In realtà, fetch() va in catch soprattutto per errori di rete (DNS, offline, CORS bloccato dal browser, ecc.). Se invece il server risponde con 404 o 500, la Promise si risolve comunque: sta a te controllare response.ok.

Ecco una versione robusta che gestisce sia errori di rete sia errori HTTP, mantenendo il codice chiaro con try catch async await:

Tip UX: nel catch restituisci un valore “safe” (es. array vuoto) se ha senso per il tuo caso d’uso, così eviti errori a cascata nell’interfaccia. Per organizzare le chiamate in un unico punto, vedi anche Fetch API e chiamate HTTP pulite in JavaScript.


Cosa non fare: anti-pattern comuni

1) Il catch silenzioso

Non lasciare mai il catch vuoto. Ignorare un errore è spesso peggio che far crashare l’app: rende impossibile il debugging e nasconde bug reali in produzione.

2) Mischiare .then() con try/catch nella stessa funzione

Se usi async/await, sii coerente: non mescolare .then().catch() e try...catch nella stessa funzione. La leggibilità cala e diventa più difficile capire il flusso degli errori.

3) Lanciare errori “generici” senza contesto

Quando fai throw new Error(...), includi dettagli utili (endpoint, status, operazione) e logga in modo strutturato. In produzione, la differenza tra “Errore generico” e “GET /api/prodotti 500” è enorme.


Domande frequenti

Async/await sostituisce le Promise?

No: async/await è costruito sopra le promesse. Una funzione async restituisce sempre una Promise e await attende che una Promise si risolva. Cambia la sintassi, non il meccanismo.

Dove posso usare await?

All’interno di una funzione dichiarata async e, nei moduli ES, anche al livello più alto del file (top-level await). In una funzione normale await genera un errore di sintassi.

Perché il catch non intercetta un errore 404 di fetch?

Perché fetch() rifiuta la Promise solo per errori di rete. Una risposta 404 o 500 è comunque una risposta valida: devi controllare response.ok e lanciare tu l’errore con throw.

A cosa serve finally con async/await?

Il blocco finally viene eseguito sempre, dopo il try o dopo il catch: è il posto giusto per nascondere uno spinner, chiudere una connessione o ripristinare lo stato dell’interfaccia.

Conclusione: perché adottare async/await

In sintesi, async/await non introduce nuove funzionalità, ma migliora drasticamente la leggibilità e la manutenibilità del codice asincrono. I principali vantaggi sono:

  • Codice più pulito: meno annidamenti e callback, più logica lineare.
  • Gestione degli errori semplificata: uso dei familiari blocchi try...catch, con finally per la pulizia.
  • Debugging più facile: è più semplice seguire il flusso di esecuzione passo dopo passo.

Con due accorgimenti (controllo di response.ok e finally per la pulizia) puoi rendere la tua app molto più resiliente. Ricorda: un buon sviluppatore non è chi scrive codice che non fallisce mai, ma chi scrive codice che sa fallire bene quando succede.

Risorse autorevoli: MDN: try…catch e MDN: Fetch API.