Introduzione
Il costrutto try...catch permette di intercettare gli errori durante l’esecuzione di JavaScript senza interrompere necessariamente l’intero programma. Tuttavia, usare catch non significa semplicemente “nascondere” un problema. Una gestione efficace deve stabilire dove catturare l’errore, come trasformarlo in un’informazione utile e quando lasciarlo propagare verso un livello superiore.
Questo approccio è particolarmente importante nelle applicazioni organizzate in più livelli, ad esempio:
- un modulo che legge dati da un servizio esterno;
- un servizio che applica la logica dell’applicazione;
- un’interfaccia che mostra un messaggio all’utente.
Se ogni funzione cattura gli errori e li ignora, diventa difficile capire cosa sia andato storto. Al contrario, una funzione può aggiungere contesto all’errore e rilanciarlo con throw, lasciando al chiamante la decisione finale. In questo tutorial vedremo come costruire una catena di gestione degli errori con try...catch, come preservare l’errore originale e come evitare i più comuni anti-pattern.
Codice completo
class ServiceError extends Error {
constructor(message, options = {}) {
super(message, options);
this.name = "ServiceError";
}
}
// Simula un modulo che comunica con un archivio dati
function loadProductFromDatabase(productId) {
if (!Number.isInteger(productId) || productId <= 0) {
throw new Error("L´identificativo del prodotto non è valido");
}
// Simuliamo un errore tecnico del database
throw new Error("Connessione al database non disponibile");
}
// Livello di servizio: aggiunge contesto e propaga l´errore
function getProduct(productId) {
try {
return loadProductFromDatabase(productId);
} catch (error) {
// L´errore originale viene conservato in cause
throw new ServiceError(
`Impossibile recuperare il prodotto ${productId}`,
{ cause: error }
);
}
}
// Livello applicativo: decide come reagire ai diversi problemi
function showProduct(productId) {
try {
const product = getProduct(productId);
console.log("Prodotto trovato:", product);
} catch (error) {
if (error instanceof ServiceError) {
console.error("Errore del servizio:", error.message);
// Accediamo alla causa tecnica originale
if (error.cause) {
console.error("Dettaglio tecnico:", error.cause.message);
}
console.log("Messaggio da mostrare all´utente:");
console.log("Il prodotto non è momentaneamente disponibile.");
return;
}
// Gli errori inattesi non vengono ignorati
throw error;
}
}
showProduct(42); Spiegazione
1. Catturare solo dove è possibile agire
Nel codice, loadProductFromDatabase() genera un errore tecnico. Questa funzione non sa come comunicarlo all’utente, quindi non dovrebbe occuparsi dell’interfaccia. Il suo compito è segnalare il problema usando throw.
La funzione getProduct(), invece, rappresenta un livello di servizio. Qui l’errore viene intercettato con try...catch per aggiungere informazioni utili: sappiamo che il problema è avvenuto durante il recupero di uno specifico prodotto.
La sintassi generale è:
try {
// Operazione che può generare un´eccezione
} catch (error) {
// Gestione o trasformazione dell´errore
throw error;
} 2. Rilanciare un errore con contesto
Rilanciare l’errore originale con throw error è possibile, ma spesso non fornisce abbastanza informazioni. In un’applicazione reale è utile sapere quale operazione di alto livello è fallita. Per questo l’esempio crea un ServiceError e collega l’errore iniziale tramite la proprietà cause.
La proprietà cause è supportata dagli ambienti JavaScript moderni e consente di mantenere la relazione tra errore principale e causa originale:
try {
// ...
} catch (error) {
throw new Error("Operazione fallita", {
cause: error
});
} In questo modo il messaggio destinato all’utente può rimanere semplice, mentre i dettagli tecnici restano disponibili per il log o per il debugging.
3. Distinguere gli errori gestibili da quelli inattesi
Nel livello applicativo, instanceof ServiceError permette di riconoscere gli errori che il programma sa gestire. Per questi casi viene mostrato un messaggio comprensibile e l’esecuzione termina in modo controllato.
Gli errori sconosciuti, invece, vengono rilanciati:
catch (error) {
if (error instanceof ServiceError) {
console.log("Errore previsto");
return;
}
throw error;
} Ignorare un errore inatteso con un semplice console.log può nascondere bug importanti. Rilanciarlo consente a un gestore globale, a un framework o a un sistema di monitoraggio di rilevarlo.
Best practice
- Non usare catch vuoti. Un blocco
catchsenza istruzioni nasconde il problema e rende il debugging molto più difficile. - Cattura gli errori al livello corretto. Il modulo tecnico dovrebbe descrivere il problema, mentre il livello dell’interfaccia dovrebbe decidere cosa mostrare all’utente.
- Preserva l’errore originale. Usa
causequando trasformi un errore, così non perdi la traccia tecnica dell’eccezione. - Non mostrare dettagli sensibili. Messaggi contenenti query, percorsi interni o credenziali non devono essere visualizzati direttamente all’utente.
- Usa messaggi specifici. “Operazione fallita” è meno utile di “Impossibile recuperare il prodotto 42”.
- Rilancia gli errori sconosciuti. Se non sai gestire un’eccezione, non fingere che tutto sia andato bene.
- Non usare try...catch per il normale controllo del flusso. Le eccezioni devono rappresentare condizioni anomale, non sostituire normali istruzioni
if. - Registra gli errori in modo sicuro. Nei sistemi reali usa un logger centralizzato, evitando di memorizzare dati personali o segreti.
Riepilogo
try...catch è più efficace quando viene inserito in una strategia a livelli. Le funzioni a basso livello possono generare errori tecnici, i servizi possono aggiungere contesto e il livello applicativo può decidere come reagire.
La combinazione di throw, catch, controllo del tipo di errore e proprietà cause permette di ottenere codice più leggibile e diagnosticabile. La regola principale è semplice: cattura un errore solo se puoi gestirlo o arricchirlo; altrimenti lascialo propagare.
