Meilleures pratiques pour intercepter et relancer les exceptions .NET

284

Quelles sont les meilleures pratiques à prendre en compte lors de la capture d'exceptions et de leur réexécution? Je veux m'assurer que l' Exceptionobjet InnerExceptionet la trace de pile sont préservés. Y a-t-il une différence entre les blocs de code suivants dans la façon dont ils gèrent cela?

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

Contre:

try
{
    //some code
}
catch
{
    throw;
}
Seibar
la source

Réponses:

262

La façon de conserver la trace de la pile consiste à utiliser le throw;Ceci est également valable

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

throw ex;revient à lancer une exception à partir de ce point, de sorte que la trace de la pile n'irait qu'à l'endroit où vous émettez l' throw ex;instruction.

Mike a également raison, en supposant que l'exception vous permet de passer une exception (ce qui est recommandé).

Karl Seguin a également une excellente écriture sur la gestion des exceptions dans ses fondements de la programmation de livres électroniques , ce qui est une excellente lecture.

Edit: lien de travail vers les fondations de la programmation pdf. Recherchez simplement «exception» dans le texte.

Darren Kopp
la source
10
Je ne suis pas sûr que cette écriture soit merveilleuse, elle suggère d'essayer {// ...} catch (Exception ex) {throw new Exception (ex.Message + "other stuff"); } est bon. Le problème est que vous êtes complètement incapable de gérer cette exception plus loin dans la pile, à moins que vous n'attrapiez toutes les exceptions, un gros non-non (vous êtes sûr de vouloir gérer cette OutOfMemoryException?)
ljs
2
@ljs L'article a-t-il changé depuis votre commentaire car je ne vois aucune section où il le recommande. Au contraire, en fait, il dit de ne pas le faire et vous demande si vous souhaitez également gérer l'OutOfMemoryException!?
RyanfaeScotland
6
Parfois jeter; n'est pas suffisant pour conserver la trace de la pile. Voici un exemple https://dotnetfiddle.net/CkMFoX
Artavazd Balayan
4
Ou ExceptionDispatchInfo.Capture(ex).Throw(); throw;dans .NET +4.5 stackoverflow.com/questions/57383/…
Alfred Wallace
La solution @AlfredWallace a parfaitement fonctionné pour moi. try {...} catch {throw} n'a pas conservé la trace de la pile. Je vous remercie.
atownson
100

Si vous lancez une nouvelle exception avec l'exception initiale, vous préserverez également la trace de pile initiale.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
Mike
la source
Peu importe ce que j'essaie de lancer, une nouvelle exception ("message", ex) lève toujours ex et ignore le message personnalisé. jeter une nouvelle exception ("message", ex.InnerException) fonctionne cependant.
Tod
Si aucune exception personnalisée n'est nécessaire, on peut utiliser AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Nikos Tsokos
AggregateExceptionne doit être utilisé que pour des exceptions sur des opérations agrégées. Par exemple, il est lancé par les classes ParallelEnumerableet Taskdu CLR. L'utilisation devrait probablement suivre cet exemple.
Aluan Haddad
29

En fait, il existe certaines situations dans lesquelles la throwdéclaration ne conservera pas les informations StackTrace. Par exemple, dans le code ci-dessous:

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

Le StackTrace indiquera que la ligne 54 a soulevé l'exception, bien qu'elle ait été soulevée à la ligne 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

Dans des situations comme celle décrite ci-dessus, il existe deux options pour préconfigurer le StackTrace d'origine:

Appel de l'exception.InternalPreserveStackTrace

Comme il s'agit d'une méthode privée, elle doit être invoquée en utilisant la réflexion:

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

J'ai un inconvénient à compter sur une méthode privée pour conserver les informations StackTrace. Il pourra être modifié dans les futures versions de .NET Framework. L'exemple de code ci-dessus et la solution proposée ci-dessous ont été extraits du blog Fabrice MARGUERIE .

Appel d'Exception.SetObjectData

La technique ci-dessous a été suggérée par Anton Tykhyy comme réponse à In C #, comment puis-je relancer InnerException sans perdre la question de trace de pile .

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 
} 

Bien qu'il ait l'avantage de s'appuyer uniquement sur des méthodes publiques, il dépend également du constructeur d'exceptions suivant (que certaines exceptions développées par des tiers ne mettent pas en œuvre):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

Dans ma situation, j'ai dû choisir la première approche, car les exceptions soulevées par une bibliothèque tierce que j'utilisais n'ont pas implémenté ce constructeur.

CARLOS LOTH
la source
1
Vous pouvez intercepter l'exception et publier cette exception où vous le souhaitez. Ensuite, jetez-en un nouveau expliquant ce qui est arrivé à l'utilisateur. De cette façon, vous pouvez voir ce qui s'est passé à l'heure actuelle à laquelle l'exception a été interceptée, l'utilisateur peut donc se méprendre sur l'exception réelle.
Çöđěxěŕ
2
Avec .NET 4.5, il existe une troisième et, à mon avis, une option plus propre: utilisez ExceptionDispatchInfo. Voir la réponse des Tragedians à une question connexe ici: stackoverflow.com/a/17091351/567000 pour plus d'informations.
Søren Boisen
20

Lorsque vous throw ex, vous jetez essentiellement une nouvelle exception et vous manquerez les informations de trace de pile d'origine. throwest la méthode préférée.

Point-virgule oublié
la source
13

La règle d'or est d'éviter d'attraper et de lancer l' Exceptionobjet de base . Cela vous oblige à être un peu plus intelligent sur les exceptions; en d'autres termes, vous devriez avoir un catch explicite pour a SqlExceptionafin que votre code de manipulation ne fasse pas quelque chose de mal avec a NullReferenceException.

Dans le monde réel, cependant, attraper et enregistrer l'exception de base est également une bonne pratique, mais n'oubliez pas de parcourir le tout pour en obtenir InnerExceptions.

swilliams
la source
2
Je pense qu'il est préférable de traiter les exceptions non gérées à des fins de journalisation en utilisant les exceptions AppDomain.CurrentDomain.UnhandledException et Application.ThreadException. Utiliser de gros blocs try {...} catch (Exception ex) {...} partout signifie beaucoup de duplication. Dépend si vous souhaitez enregistrer les exceptions gérées, auquel cas (au moins minime) la duplication peut être inévitable.
ljs
De plus en utilisant ces moyens d'événements que vous faites enregistrer toutes les exceptions non gérées, alors que si vous utilisez grand ol » try {...} catch (Exception ex) blocs {...} vous risquez de manquer quelques - uns.
ljs
10

Vous devez toujours utiliser "throw;" pour renvoyer les exceptions dans .NET,

Reportez-vous à ceci, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

Fondamentalement, MSIL (CIL) a deux instructions - "lancer" et "relancer":

  • "Throw ex;" de C # est compilé dans le "lancer" de MSIL
  • "Lancer" de C #; - dans MSIL "rethrow"!

Fondamentalement, je peux voir la raison pour laquelle "throw ex" remplace la trace de la pile.

Vinod T. Patil
la source
Le lien - enfin, en fait la source que cite le lien - est plein de bonnes informations, et note également un coupable possible pour la raison pour laquelle beaucoup pensent throw ex;retourner: en Java, c'est le cas! Mais vous devez inclure cette information ici pour avoir une réponse de catégorie A. (Bien que je rattrape toujours la ExceptionDispatchInfo.Captureréponse de jeuoekdcwzfwccu .)
ruffin
10

Personne n'a expliqué la différence entre ExceptionDispatchInfo.Capture( ex ).Throw()et une plaine throw, alors la voici. Cependant, certaines personnes ont remarqué le problème avec throw.

La manière complète de renvoyer une exception interceptée consiste à utiliser ExceptionDispatchInfo.Capture( ex ).Throw()(uniquement disponible à partir de .Net 4.5).

Vous trouverez ci-dessous les cas nécessaires pour tester cela:

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 );
    }
}

Le cas 1 et le cas 2 vous donneront une trace de pile où le numéro de ligne de code source pour la CallingMethodméthode est le numéro de throw new Exception( "TEST" )ligne de la ligne.

Cependant, le cas 3 vous donnera une trace de pile où le numéro de ligne de code source pour la CallingMethodméthode est le numéro de ligne de l' throwappel. Cela signifie que si la throw new Exception( "TEST" )ligne est entourée d'autres opérations, vous ne savez pas à quel numéro de ligne l'exception a été levée.

Le cas 4 est similaire au cas 2 parce que le numéro de ligne de l'exception d'origine est conservé, mais n'est pas un véritable renversement car il change le type de l'exception d'origine.

jeuoekdcwzfwccu
la source
Ajoutez un simple texte de présentation à ne jamais utiliser throw ex;et c'est la meilleure réponse de tous.
NH.
8

Quelques personnes ont en fait raté un point très important - «jeter» et «jeter ex» peut faire la même chose mais ils ne vous donnent pas une information cruciale qui est la ligne où l'exception s'est produite.

Considérez le code suivant:

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
    }
}

Lorsque vous effectuez un `` lancer '' ou un `` lancer ex '', vous obtenez la trace de la pile, mais la ligne # va être # 22, vous ne pouvez donc pas savoir quelle ligne exactement lançait l'exception (sauf si vous n'en avez que 1 ou quelques lignes de code dans le bloc try). Pour obtenir la ligne n ° 17 attendue dans votre exception, vous devrez lancer une nouvelle exception avec la trace de pile d'exceptions d'origine.

notlkk
la source
3

Vous pouvez également utiliser:

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

Et toutes les exceptions levées vont remonter au niveau suivant qui les gère.

Erick B
la source
3

J'utiliserais certainement:

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;
}

Cela préservera votre pile.

1kevgriff
la source
1
Pour être juste envers moi en 2008, le PO demandait comment conserver la pile - et 2008 m'a donné une réponse correcte. Ce qui manque à ma réponse, c'est la partie de faire quelque chose dans le piège.
1kevgriff
@JohnSaunders C'est vrai si et seulement si vous ne faites rien avant throw; par exemple, vous pouvez nettoyer un jetable (où vous l'appelez UNIQUEMENT en cas d'erreur), puis lever l'exception.
Meirion Hughes
@meirion quand j'ai écrit le commentaire, il n'y avait rien avant le lancer. Lorsque cela a été ajouté, j'ai voté, mais je n'ai pas supprimé le commentaire.
John Saunders
0

Pour info je viens de tester ceci et la trace de pile rapportée par 'throw;' n'est pas une trace de pile entièrement correcte. Exemple:

    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 trace de pile pointe correctement vers l'origine de l'exception (numéro de ligne signalé) mais le numéro de ligne signalé pour foo () est la ligne du lancer; , par conséquent, vous ne pouvez pas dire lequel des appels à bar () a provoqué l'exception.

redcalx
la source
C'est pourquoi il est préférable de ne pas essayer d'attraper des exceptions à moins que vous ne prévoyiez de faire quelque chose avec elles
Nate Zaugg