Introduzione
Quando si lavora con le Promise, .catch() non serve soltanto a mostrare un messaggio d’errore. Può anche permettere di recuperare da un problema, fornire un valore alternativo e lasciare proseguire l’esecuzione del programma. Questa tecnica è particolarmente utile quando un’applicazione deve continuare a funzionare anche se una risorsa secondaria non è disponibile.
Un caso reale è il caricamento della configurazione di un’applicazione. Se il server risponde correttamente, utilizziamo i dati ricevuti. Se invece la richiesta fallisce, possiamo usare una configurazione predefinita. L’errore viene quindi gestito localmente, senza interrompere tutta la catena di Promise.
È importante distinguere tra due comportamenti:
- Gestire l’errore: il .catch() restituisce un valore alternativo e la catena continua.
- Rilanciare l’errore: il .catch() esegue alcune operazioni, poi restituisce Promise.reject() o usa throw per delegare la gestione a un livello superiore.
Questa separazione consente di creare codice più robusto e di decidere dove un errore debba essere risolto e dove, invece, debba essere registrato o mostrato all’utente.
Codice completo
// Simula il caricamento di una configurazione da un server
function caricaConfigurazione() {
return fetch("/configurazione.json")
.then(response => {
if (!response.ok) {
throw new Error(`Errore HTTP: ${response.status}`);
}
return response.json();
});
}
// Configurazione utilizzata quando il server non è disponibile
function configurazionePredefinita() {
return {
tema: "chiaro",
lingua: "it",
elementiPerPagina: 10
};
}
// Avvia l´applicazione
caricaConfigurazione()
.catch(errore => {
console.warn("Configurazione remota non disponibile:", errore.message);
// Restituire un oggetto normale equivale a una Promise risolta
return configurazionePredefinita();
})
.then(configurazione => {
console.log("Configurazione attiva:", configurazione);
if (!configurazione.lingua) {
throw new Error("La configurazione non contiene la lingua");
}
return configuraInterfaccia(configurazione);
})
.catch(errore => {
// Questo catch intercetta anche gli errori generati nel then precedente
console.error("Impossibile avviare l´interfaccia:", errore.message);
// Rilancio l´errore per permettere a un livello ancora superiore di gestirlo
throw errore;
})
.catch(errore => {
// Punto finale della gestione dell´errore
mostraMessaggioErrore("L´applicazione non può essere avviata.");
});
function configuraInterfaccia(configurazione) {
return new Promise((resolve, reject) => {
try {
document.body.dataset.tema = configurazione.tema;
document.documentElement.lang = configurazione.lingua;
resolve("Interfaccia configurata");
} catch (errore) {
reject(errore);
}
});
}
function mostraMessaggioErrore(messaggio) {
console.error(messaggio);
} Spiegazione
Gestire un errore con un valore alternativo
Il primo .catch() intercetta un eventuale errore prodotto da caricaConfigurazione(). Invece di interrompere il flusso, registra il problema e restituisce il risultato di configurazionePredefinita().
Quando una funzione interna a .catch() restituisce un valore normale, la Promise risultante viene considerata risolta. Per questo motivo il .then() successivo riceve comunque una configurazione valida:
Promise.reject(new Error("Errore"))
.catch(() => {
return "valore alternativo";
})
.then(valore => {
console.log(valore); // valore alternativo
}); Questo schema è chiamato spesso fallback: se il percorso principale fallisce, viene utilizzato un percorso secondario.
Quando usare un secondo catch
Il .catch() non intercetta soltanto il rifiuto iniziale. Intercetta anche gli errori lanciati dai .then() precedenti. Nel codice completo, la validazione della configurazione può generare un nuovo errore con throw. Questo errore viene ricevuto dal secondo .catch().
Il secondo gestore esegue una registrazione più generale e poi rilancia l’errore. Rilanciare è utile quando il livello corrente non ha abbastanza informazioni per concludere la gestione. Il problema viene quindi trasferito al .catch() finale, responsabile della comunicazione all’utente.
Perché evitare catch troppo generici
Un .catch() posizionato troppo presto potrebbe nascondere errori importanti. Ad esempio, usare una configurazione predefinita è sensato se fallisce la richiesta remota, ma potrebbe essere pericoloso se i dati ricevuti sono formalmente validi ma contengono informazioni corrotte.
La gestione dovrebbe quindi essere specifica: recuperare soltanto dagli errori che l’applicazione sa davvero risolvere e rilanciare gli altri.
Best practice
- Usa fallback appropriati: un valore alternativo deve essere realmente compatibile con ciò che il codice successivo si aspetta.
- Non ignorare gli errori: anche se puoi recuperare, registra almeno un’informazione utile per il debugging.
- Rilancia gli errori non gestibili: usa throw errore quando il livello corrente non può risolvere il problema.
- Conserva il contesto: puoi aggiungere informazioni all’errore, mantenendo però la causa originale.
- Evita duplicazioni: colloca la gestione finale in un punto chiaro, invece di mostrare più volte lo stesso messaggio.
- Controlla le risposte HTTP: fetch() non considera automaticamente un errore HTTP, come il codice 404, un rifiuto della Promise. È necessario verificare response.ok.
- Non usare catch vuoti: un blocco senza log o conseguenze rende gli errori difficili da diagnosticare.
Riepilogo
Più blocchi .catch() possono essere utilizzati per creare livelli distinti di gestione degli errori. Un primo livello può fornire un fallback e permettere alla catena di continuare. Un livello successivo può validare i dati, registrare problemi più gravi e rilanciare gli errori. Infine, un gestore globale può mostrare un messaggio comprensibile all’utente.
La regola principale è semplice: gestisci localmente solo gli errori che sai recuperare; per gli altri, conserva il contesto e rilanciali verso un livello responsabile della decisione finale.
