Best practice per la cattura e il rilancio delle eccezioni .NET


284

Quali sono le migliori pratiche da prendere in considerazione quando si rilevano le eccezioni e le si respingono? Voglio assicurarmi che l' Exceptionoggetto InnerExceptione la traccia dello stack siano preservati. C'è una differenza tra i seguenti blocchi di codice nel modo in cui gestiscono questo?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

vs:

try
{
    //some code
}
catch
{
    throw;
}

Risposte:


262

Il modo per preservare la traccia dello stack è attraverso l'uso di throw;Questo è valido anche

try {
  // something that bombs here
} catch (Exception ex)
{
    throw;
}

throw ex;è fondamentalmente come lanciare un'eccezione da quel punto, quindi la traccia dello stack andrebbe solo dove si sta emettendo l' throw ex;istruzione.

Mike ha anche ragione, supponendo che l'eccezione ti permetta di passare un'eccezione (che è consigliata).

Karl Seguin ha scritto un ottimo articolo sulla gestione delle eccezioni anche nelle sue basi della programmazione di e-book , il che è un'ottima lettura.

Modifica: link funzionante alle basi della programmazione pdf. Cerca nel testo "eccezione".


10
Non sono così sicuro che la scrittura sia meravigliosa, suggerisce di provare {// ...} catch (Exception ex) {lanciare nuova Exception (ex.Message + "other stuff"); } è buono. Il problema è che non sei completamente in grado di gestire l'eccezione più in alto nello stack, a meno che non catturi tutte le eccezioni, un grande no-no (sei sicuro di voler gestire OutOfMemoryException?)
ljs

2
@ljs L'articolo è cambiato dopo il tuo commento perché non vedo alcuna sezione in cui lo consiglia. Anzi, al contrario, dice di non farlo e chiede se vuoi gestire anche OutOfMemoryException !?
RyanfaeScotland,

6
A volte lanciare; non è sufficiente per conservare la traccia dello stack. Ecco un esempio https://dotnetfiddle.net/CkMFoX
Artavazd Balayan

4
O ExceptionDispatchInfo.Capture(ex).Throw(); throw;in .NET +4.5 stackoverflow.com/questions/57383/…
Alfred Wallace

La soluzione @AlfredWallace ha funzionato perfettamente per me. try {...} catch {throw} non ha conservato la traccia dello stack. Grazie.
atownson,

100

Se si genera una nuova eccezione con l'eccezione iniziale, verrà preservata anche la traccia dello stack iniziale.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}

Indipendentemente da ciò che provo a generare una nuova eccezione ("messaggio", ex) genera sempre ex e ignora il messaggio personalizzato. lanciare nuove eccezioni ("messaggio", ex.InnerException) funziona però.
Tod

Se non è necessaria alcuna eccezione personalizzata, è possibile utilizzare AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Nikos Tsokos,

AggregateExceptiondovrebbe essere utilizzato solo per eccezioni rispetto alle operazioni aggregate. Ad esempio, viene generato dalle classi ParallelEnumerablee Taskdel CLR. L'utilizzo dovrebbe probabilmente seguire questo esempio.
Aluan Haddad,

29

In realtà, ci sono alcune situazioni in cui la throwdichiarazione non conserverà le informazioni StackTrace. Ad esempio, nel codice seguente:

try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

StackTrace indicherà che la riga 54 ha sollevato l'eccezione, sebbene sia stata sollevata alla riga 47.

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowIncomplete() in Program.cs:line 54
   at Program.Main(String[] args) in Program.cs:line 106

In situazioni come quella sopra descritta, ci sono due opzioni per preconfigurare lo StackTrace originale:

Chiamata Exception.InternalPreserveStackTrace

Essendo un metodo privato, deve essere invocato usando reflection:

private static void PreserveStackTrace(Exception exception)
{
  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);
}

Ho uno svantaggio di fare affidamento su un metodo privato per preservare le informazioni StackTrace. Può essere modificato nelle versioni future di .NET Framework. L'esempio di codice sopra e la soluzione proposta sotto sono stati estratti dal blog di Fabrice MARGUERIE .

Chiamata Exception.SetObjectData

La tecnica seguente è stata suggerita da Anton Tykhyy come risposta a In C #, come posso riproporre InnerException senza perdere la domanda di stack stack .

static void PreserveStackTrace (Exception e) 
{ 
  var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ; 
  var mgr = new ObjectManager     (null, ctx) ; 
  var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ; 

  e.GetObjectData    (si, ctx)  ; 
  mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
  mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData 

  // voila, e is unmodified save for _remoteStackTraceString 
} 

Sebbene abbia il vantaggio di basarsi solo su metodi pubblici, dipende anche dal seguente costruttore di eccezioni (che alcune eccezioni sviluppate da terze parti non implementano):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

Nella mia situazione, ho dovuto scegliere il primo approccio, perché le eccezioni sollevate da una libreria di terze parti che stavo usando non implementavano questo costruttore.


1
Puoi prendere l'eccezione e pubblicare questa eccezione ovunque tu voglia. Quindi lanciane uno nuovo spiegando cosa è successo all'utente. In questo modo puoi vedere cosa è successo nel momento in cui l'eccezione è stata rilevata, l'utente può ignorare quale fosse l'eccezione effettiva.
Çöđěxěà

2
Con .NET 4.5 esiste una terza e, a mio avviso, un'opzione più pulita: utilizzare ExceptionDispatchInfo. Vedi Tragedians rispondere a una domanda correlata qui: stackoverflow.com/a/17091351/567000 per maggiori informazioni.
Søren Boisen,

20

Quando throw exstai essenzialmente lanciando una nuova eccezione, perderai le informazioni di traccia dello stack originale. throwè il metodo preferito.


13

La regola empirica è quella di evitare la cattura e il lancio Exceptiondell'oggetto base . Questo ti obbliga ad essere un po 'più intelligente riguardo alle eccezioni; in altre parole dovresti avere una cattura esplicita per a in SqlExceptionmodo che il tuo codice di gestione non faccia qualcosa di sbagliato in a NullReferenceException.

Nel mondo reale, tuttavia, catturare e registrare l'eccezione di base è anche una buona pratica, ma non dimenticare di camminare tutto per ottenere tutto InnerExceptionsciò che potrebbe avere.


2
Penso che sia meglio gestire le eccezioni non gestite ai fini della registrazione utilizzando le eccezioni AppDomain.CurrentDomain.UnhandledException e Application.ThreadException. L'uso di blocchi big try {...} catch (Exception ex) {...} ovunque significa molta duplicazione. Dipende se si desidera registrare le eccezioni gestite, nel qual caso la duplicazione (almeno minima) potrebbe essere inevitabile.
ljs

Inoltre, l'utilizzo di tali eventi significa che si registrano tutte le eccezioni non gestite, mentre se si utilizzano blocchi big ol 'try {...} catch (Exception ex) {...} potreste perdere alcuni.
ljs

10

Dovresti sempre usare "lancio;" per riproporre le eccezioni in .NET,

Fare riferimento a questo, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

Fondamentalmente MSIL (CIL) ha due istruzioni: "lanciare" e "riprovare":

  • "Buttare ex;" di C # viene compilato nel "lancio" di MSIL
  • "Lancio" di C # - in MSIL "ripensare"!

Fondamentalmente posso vedere il motivo per cui "buttare ex" sovrascrive la traccia dello stack.


Il link - beh, in realtà la fonte che cita il link - è pieno di buone informazioni e nota anche un possibile colpevole del motivo per cui molti pensano che throw ex;si riproporranno - in Java, lo fa! Ma dovresti includere queste informazioni qui per avere una risposta di grado A. (Anche se sto ancora raggiungendo la ExceptionDispatchInfo.Capturerisposta di jeuoekdcwzfwccu .)
ruffin

10

Nessuno ha spiegato la differenza tra ExceptionDispatchInfo.Capture( ex ).Throw()e una pianura throw, quindi eccola qui. Tuttavia, alcune persone hanno notato il problema throw.

Il modo completo per ricodificare un'eccezione rilevata è utilizzare ExceptionDispatchInfo.Capture( ex ).Throw()(disponibile solo da .Net 4.5).

Di seguito sono riportati i casi necessari per testare questo:

1.

void CallingMethod()
{
    //try
    {
        throw new Exception( "TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    }
}

3.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch
    {
        throw;
    }
}

4.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        throw new Exception( "RETHROW", ex );
    }
}

Il caso 1 e il caso 2 forniscono una traccia dello stack in cui il numero di riga del codice sorgente per il CallingMethodmetodo è il numero di riga delthrow new Exception( "TEST" ) riga riga.

Tuttavia, il caso 3 fornisce una traccia dello stack in cui il numero di riga del codice sorgente per il CallingMethodmetodo è il numero di riga della throwchiamata. Ciò significa che se la throw new Exception( "TEST" )linea è circondata da altre operazioni, non si ha idea di quale numero di riga sia stata effettivamente generata l'eccezione.

Il caso 4 è simile al caso 2 perché il numero di riga dell'eccezione originale viene conservato, ma non è un vero e proprio rilancio perché modifica il tipo di eccezione originale.


Aggiungi un semplice blurb da non usare mai throw ex;e questa è la migliore risposta a tutti.
NH.

8

Alcune persone in realtà hanno perso un punto molto importante: "lanciare" e "lanciare ex" possono fare la stessa cosa ma non ti danno un pezzo cruciale di informazioni che è la linea in cui si è verificata l'eccezione.

Considera il seguente codice:

static void Main(string[] args)
{
    try
    {
        TestMe();
    }
    catch (Exception ex)
    {
        string ss = ex.ToString();
    }
}

static void TestMe()
{
    try
    {
        //here's some code that will generate an exception - line #17
    }
    catch (Exception ex)
    {
        //throw new ApplicationException(ex.ToString());
        throw ex; // line# 22
    }
}

Quando esegui un 'lancio' o un 'lancio ex' ottieni la traccia dello stack ma la linea # sarà # 22 quindi non puoi capire quale linea esattamente ha lanciato l'eccezione (a meno che tu non abbia solo 1 o pochi righe di codice nel blocco try). Per ottenere la riga n. 17 prevista nella tua eccezione, devi generare una nuova eccezione con la traccia dello stack delle eccezioni originale.


3

Puoi anche usare:

try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

E tutte le eccezioni generate passeranno al livello successivo che le gestisce.


3

Userei sicuramente:

try
{
    //some code
}
catch
{
    //you should totally do something here, but feel free to rethrow
    //if you need to send the exception up the stack.
    throw;
}

Ciò preserverà il tuo stack.


1
Per essere onesti nel superarmi nel 2008, l'OP stava chiedendo come conservare lo stack e nel 2008 ho dato una risposta corretta. Quello che manca alla mia risposta è la parte di fare qualcosa di veramente efficace.
1kevgriff il

@JohnSaunders Questo è vero se e solo se non hai fatto nulla prima throw; ad esempio, è possibile ripulire un dispositivo monouso (dove lo si chiama SOLO in caso di errore) e quindi generare l'eccezione.
Meirion Hughes,

@meirion quando ho scritto il commento non c'era nulla prima del lancio. Quando è stato aggiunto, ho effettuato l'upgrade, ma non ho eliminato il commento.
John Saunders,

0

Cordiali saluti, ho appena provato questo e la traccia dello stack riportata da 'throw;' non è una traccia dello stack completamente corretta. Esempio:

    private void foo()
    {
        try
        {
            bar(3);
            bar(2);
            bar(1);
            bar(0);
        }
        catch(DivideByZeroException)
        {
            //log message and rethrow...
            throw;
        }
    }

    private void bar(int b)
    {
        int a = 1;
        int c = a/b;  // Generate divide by zero exception.
    }

La traccia dello stack punta correttamente all'origine dell'eccezione (numero di riga riportato) ma il numero di riga riportato per foo () è la riga del lancio; , quindi non puoi dire quale delle chiamate a bar () ha causato l'eccezione.


Questo è il motivo per cui è meglio non cercare di catturare le eccezioni se non si prevede di fare qualcosa con loro
Nate Zaugg,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.