Menu

Try catch in C#: gestire le eccezioni con catch, when e finally

try, catch e finally sono il modo in cui C# gestisce le eccezioni. Scopri come un'eccezione risale lo stack delle chiamate, come catturare tipi di eccezione specifici nell'ordine giusto, filtrare con when, fare pulizia in finally, rilanciare con throw; senza perdere lo stack trace, e quali eccezioni non catturare.

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

Quando qualcosa va storto a runtime (un file manca, un testo non è un numero, una chiave non è nel dizionario), .NET lancia un'eccezione: un oggetto che descrive l'errore. L'eccezione risale lo stack delle chiamate finché un blocco catch non la gestisce. Se nessuno lo fa, il programma si ferma e stampa l'errore.

Un try catch di base

Racchiudi in try il codice che può fallire, e gestisci l'errore in catch:

Output:

42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running

Quando int.Parse("forty-two") lancia un'eccezione, il Console.WriteLine che la segue nel blocco try viene saltato, viene eseguito il blocco catch (FormatException) e il ciclo continua. Puoi omettere la variabile (catch (FormatException)) quando non ti serve l'oggetto eccezione.

Per questo caso particolare esiste uno strumento migliore: int.TryParse(input, out int n) restituisce false invece di lanciare un'eccezione. Le eccezioni servono per le situazioni che il codice non si aspetta; un input spesso non valido è prevedibile, quindi controllalo invece di catturare l'eccezione.

Come viaggia un'eccezione

Un'eccezione lanciata in profondità in una catena di chiamate srotola ogni metodo finché non trova un gestore corrispondente. In ciascuno di quei metodi, il codice dopo il lancio non viene eseguito.

Output:

OrderTotal finished
5.00
Caught KeyNotFoundException in Main

PriceOf e OrderTotal non hanno un catch, quindi l'eccezione li attraversa dritta fino a Main. Metti un gestore al livello che sa cosa fare dell'errore, che spesso non è quello in cui è avvenuto.

Catturare eccezioni specifiche, nell'ordine giusto

Una clausola catch gestisce il suo tipo e tutti i tipi che ne derivano. Quando ci sono più clausole vince la prima che corrisponde, quindi ordinale dalla più specifica alla più generica:

Output:

10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException

L'ultima chiamata lancia OverflowException, che nessuna delle clausole specifiche gestisce, quindi se ne occupa il catch (Exception e) generico. Mettere catch (Exception) per primo renderebbe irraggiungibili le clausole successive, e il compilatore lo rifiuta con l'errore CS0160.

Filtri di eccezione con when

C# 6 ha aggiunto when: una condizione che decide se una clausola catch si applica. Se è false, il runtime continua a cercare un altro gestore come se la clausola non esistesse.

Output:

Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401

I filtri ti permettono di decidere in base ai dati contenuti nell'eccezione senza catturarla e rilanciarla. Sono anche il modo pulito per gestire allo stesso modo due tipi non collegati, come fa la terza clausola. Un filtro viene eseguito prima che lo stack venga srotolato, quindi un debugger o un crash dump mostra ancora lo stato originale quando nessun filtro corrisponde.

finally: codice che viene sempre eseguito

Un blocco finally viene eseguito quando il controllo lascia il try, che sia terminato, sia uscito prima con return o abbia lanciato un'eccezione. È lì che va la pulizia. (Un'avvertenza: se nessuno cattura l'eccezione da nessuna parte, il processo può terminare senza eseguirlo.)

Output:

Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error

In ogni caso "Close connection" viene stampato prima che il risultato del metodo arrivi a Main: il finally viene eseguito dopo che il valore di return è stato calcolato ma prima che il metodo ritorni davvero. Un try può avere un finally senza alcun catch, che fa pulizia lasciando che l'eccezione prosegua verso il chiamante.

Per gli oggetti che implementano IDisposable (file, stream, connessioni), l'istruzione using scrive questo try/finally per te.

Rilanciare: throw; vs throw e;

A volte un blocco catch registra qualcosa e poi lascia proseguire l'eccezione. Il modo in cui la rilanci decide se lo stack trace sopravvive:

Output:

throw;   trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False

throw e; tratta l'eccezione come se fosse stata appena lanciata dal blocco catch, quindi i frame sottostanti, compreso il metodo in cui è avvenuto l'errore, spariscono dalla traccia. Rilancia sempre con un semplice throw;. (L'attributo NoInlining c'è solo perché il JIT potrebbe fondere un metodo così piccolo nel suo chiamante, nascondendolo da entrambe le tracce.)

Per aggiungere contesto, invece, avvolgi l'eccezione in una nuova e passa l'originale come eccezione interna: throw new ConfigException("Could not start the app", e);. L'eccezione interna conserva il proprio stack trace, e i logger stampano la catena. Scrivere i tuoi tipi di eccezione è spiegato in lanciare eccezioni.

L'oggetto Exception

Ogni eccezione deriva da System.Exception. I membri che usi di più:

MembroCosa contiene
MessageUna descrizione leggibile
GetType().NameIl tipo di eccezione, come FormatException
StackTraceLa catena di chiamate ai metodi nel punto del lancio
InnerExceptionL'eccezione che ha causato questa, oppure null
ToString()Tipo, messaggio, eccezioni interne e stack trace insieme

Registra nel log e.ToString() invece di e.Message quando vuoi diagnosticare un errore in seguito: il solo messaggio raramente dice dove si trovava il problema. Non mostrare e.ToString() agli utenti finali.

Tipi di eccezione comuni

EccezioneCausa tipica
NullReferenceExceptionChiamare un membro su un riferimento null
ArgumentNullExceptionA un metodo è stato passato null dove serve un valore
ArgumentOutOfRangeExceptionUn argomento o un indice di lista è fuori dall'intervallo consentito
IndexOutOfRangeExceptionUn indice di array è fuori dai limiti
FormatExceptionint.Parse, DateTime.Parse e simili su testo nel formato sbagliato
InvalidCastExceptionUn cast esplicito a un tipo che l'oggetto non è
InvalidOperationExceptionL'oggetto è nello stato sbagliato per la chiamata (sequenza vuota, collezione modificata)
KeyNotFoundExceptionLeggere con l'indicizzatore una chiave assente dal dizionario
DivideByZeroExceptionDivisione intera o decimal per zero
OverflowExceptionUn numero analizzato, una conversione checked o il risultato di un'aritmetica checked non entra nel tipo
FileNotFoundException, IOExceptionProblemi del file system

NullReferenceException, IndexOutOfRangeException e InvalidCastException indicano quasi sempre un bug. Correggi il codice invece di catturarle.

Non inghiottire le eccezioni

Un catch vuoto nasconde ogni errore, compresi quelli che non avevi previsto:

try
{
    SaveOrder(order);
}
catch (Exception)
{
    // nothing: the order silently was not saved
}

Il programma continua a girare come se il salvataggio fosse riuscito, e la vera causa va persa. Linee guida che mantengono onesta la gestione delle eccezioni:

  • Cattura solo ciò che sai gestire, al livello che può gestirlo.
  • Se catturi per registrare nel log, rilancia con throw; a meno che il programma non possa davvero continuare.
  • Cattura Exception solo al bordo esterno: Main, un gestore di richieste, un ciclo di lavoro.
  • Per i casi prevedibili preferisci TryParse, TryGetValue e i controlli su null alle eccezioni. Lanciare un'eccezione è lento rispetto a un controllo, mentre un blocco try che non lancia nulla non costa quasi niente.

Errori comuni

  • catch (Exception) per primo. Le clausole successive diventano irraggiungibili (CS0160).
  • throw e; per rilanciare. Cancella lo stack trace originale; usa throw;.
  • Blocchi catch vuoti. Gli errori spariscono; come minimo registrali e rilanciali.
  • Usare le eccezioni per il flusso di controllo. Valida l'input con TryParse invece di catturare FormatException.
  • Mostrare agli utenti il e.Message di un'eccezione del framework. Il testo cambia tra le versioni di .NET ed è scritto per gli sviluppatori.

Domande frequenti

Come funziona try catch in C#?

Il codice che potrebbe fallire va nel blocco try. Se un'istruzione lì dentro lancia un'eccezione, il resto del blocco viene saltato e il runtime cerca una clausola catch il cui tipo corrisponda all'eccezione, prima nel metodo corrente e poi in ciascun chiamante. Viene eseguito il primo catch che corrisponde, e l'esecuzione continua dopo l'intera istruzione try.

Il finally viene sempre eseguito in C#?

Quasi sempre: dopo che il blocco try termina normalmente, dopo che un catch gestisce un'eccezione, dopo un return o un break dentro il blocco, e quando un'eccezione lo attraversa diretta verso un catch più in alto nello stack delle chiamate. Non viene eseguito quando il processo termina prima: un processo ucciso, Environment.FailFast, una StackOverflowException, e su .NET Core e successivi un'eccezione che nessuno cattura, che termina il processo prima che i blocchi finally vengano eseguiti.

Come catturo più eccezioni in C#?

Scrivi più clausole catch, con il tipo più specifico per primo: catch (FileNotFoundException) prima di catch (IOException) prima di catch (Exception). Il compilatore rifiuta una clausola che non può mai essere raggiunta perché una precedente cattura già il suo tipo. Per gestire allo stesso modo due tipi non collegati, usa un filtro: catch (Exception e) when (e is FormatException || e is OverflowException).

Qual è la differenza tra throw e throw ex in C#?

Dentro un catch, throw; rilancia l'eccezione corrente con il suo stack trace originale. throw ex; lancia lo stesso oggetto ma reimposta lo stack trace alla riga corrente, quindi il metodo in cui l'errore è davvero avvenuto sparisce dalla traccia. Usa throw;, oppure avvolgila: throw new MyException("context", ex);.

Cos'è un filtro di eccezione in C#?

Una clausola when dopo un catch (C# 6 e successivi): catch (HttpRequestException e) when (e.Message.Contains("404")). Il blocco catch viene eseguito solo se la condizione è true; altrimenti l'eccezione continua a cercare un altro gestore come se la clausola non ci fosse, e il suo stack non viene srotolato.

Dovrei catturare Exception in C#?

Solo ai bordi di un programma: l'inizio di Main, un gestore di richieste o un ciclo in background, dove il compito è registrare l'errore e andare avanti o uscire in modo pulito. In profondità nel codice, cattura i tipi specifici che sai davvero gestire, e lascia propagare tutto il resto.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA