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ù:
| Membro | Cosa contiene |
|---|---|
Message | Una descrizione leggibile |
GetType().Name | Il tipo di eccezione, come FormatException |
StackTrace | La catena di chiamate ai metodi nel punto del lancio |
InnerException | L'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
| Eccezione | Causa tipica |
|---|---|
NullReferenceException | Chiamare un membro su un riferimento null |
ArgumentNullException | A un metodo è stato passato null dove serve un valore |
ArgumentOutOfRangeException | Un argomento o un indice di lista è fuori dall'intervallo consentito |
IndexOutOfRangeException | Un indice di array è fuori dai limiti |
FormatException | int.Parse, DateTime.Parse e simili su testo nel formato sbagliato |
InvalidCastException | Un cast esplicito a un tipo che l'oggetto non è |
InvalidOperationException | L'oggetto è nello stato sbagliato per la chiamata (sequenza vuota, collezione modificata) |
KeyNotFoundException | Leggere con l'indicizzatore una chiave assente dal dizionario |
DivideByZeroException | Divisione intera o decimal per zero |
OverflowException | Un numero analizzato, una conversione checked o il risultato di un'aritmetica checked non entra nel tipo |
FileNotFoundException, IOException | Problemi 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
Exceptionsolo al bordo esterno:Main, un gestore di richieste, un ciclo di lavoro. - Per i casi prevedibili preferisci
TryParse,TryGetValuee i controlli sunullalle eccezioni. Lanciare un'eccezione è lento rispetto a un controllo, mentre un bloccotryche 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; usathrow;.- Blocchi catch vuoti. Gli errori spariscono; come minimo registrali e rilanciali.
- Usare le eccezioni per il flusso di controllo. Valida l'input con
TryParseinvece di catturareFormatException. - Mostrare agli utenti il
e.Messagedi 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.