Mehr von Jürgen Gutsch

Mehr von Jürgen Gutsch

Empfehlungen von Jürgen Gutsch

Blog-Empfehlungen von Jürgen Gutsch

Willkommen bei ASP.NET Zone. Anmelden | Registrieren | Hilfe

Jürgen Gutsch

ASP.NET und mehr...
Best Practices: Fehlerbehandlung

In der Vergangenheit habe ich in vielen Projekten, vielen Anwendungen und auch hier im Forum immer wieder die gleichen Fehler bei der Fehlerbehandlung gesehen, was mich dazu veranlasst diesen Beitrag hier zu schreiben.

Im Grunde ist es immer folgender Fehler der beim Try&Catch gemacht wird:

try
{
    [do some errors]
}
Catch (Exception ex)
{
    log.Error(“Meine super duper Fehlermeldung”);
    throw ex;
}

Das ist dann keine Fehlerbehandlung, sondern eher Fehler verstecken!

Oftmals wird dieses Konstrukt (“Fehlerbehandlung” mochte ich es nicht nennen) in jeder Methode genutzt die aufgerufen wird. Das hat dann den Effekt, dass dieses Konstrukt logisch wie folgt verschachtelt wird:

try
{
    try
    {
        try
       {
            try
           {
                [do some errors]
          }
           Catch (Exception ex)
            {
                log.Error(“Meine erste super duper Fehlermeldung”);
                throw ex;
           }
       }
        Catch (Exception ex)
        {
            log.Error(“Meine zweite super duper Fehlermeldung”);
           throw ex;
       }
    }
    Catch (Exception ex)
    {
       log.Error(“Meine dritte super duper Fehlermeldung”);
        throw ex;
    }
}
Catch (Exception ex)
{
    log.Error(“Meine vierte super duper Fehlermeldung”);
    throw ex;
}

Oftmals wird wenigstens die aktuelle Exception dem Logger übergeben:

Catch (Exception ex)
{
    log.Error(“Meine vierte super duper Fehlermeldung”, ex);
    throw ex;
}

Aber auch das ist nur die halbe Miete.

Was soll da falsch sein ?

Schauen wir uns nochmal das erst Stück Code an. Angenommen es passiert ein Fehler und das Programm, springt in den Catch-Teil. Jetzt geht der Unsinn los:

  1. Es wird eine Unsinnige Fehlermeldung ausgegeben!
    Warum?
    Falls die Meldung noch relevant sein sollte, enthält sie keine weiteren Informationen zu der eigentlichen Exception. Im Normalfall wurde der Code allerdings im Laufe der projektphase immer wieder geändert, was zur Folge hat, das die Nachricht längst nicht mehr stimmt oder es gibt inzwischen mehrere mögliche Fehlerquellen.
  2. throw ex ist der nächste grobe Fehler: es wird zwar die selbe Exception noch einmal geworfen, mit der selben Fehlermeldung, allerdings wird der immens Wichtige StackTrace abgeschnitten (“breaking the stack”). Der StackTrace beginnt ab der Methode in der throw ex aufgerufen wird. Sollte der Fehler in einer aufgerufenen Komponente auftauchen, kann das nicht mehr ermittelt werden. Es wird immer so aussehen, als wäre der Fehler in der aktuelle Methode aufgetreten, und zwar genau dort, wo thro ex aufgerufen worden ist. Genauso wie der StackTrace geht auch die InnerEception verloren (Danke an Peter für den Hinweis.)
  3. Selbst wenn die Exception an den Logger übergeben wird ist die Fehlerbehandlung unvollständig, denn es wird nur ein unvollständiger StackTrace gelogt.

Im Falle der verschachtelten Try&Catches sieht das so aus, dass 1. keine relevanten Fehlerinformationen geloggt werden und 2. jedes Mal der StackTrace bei jedem Catch von neuem beginnt.

Diese Fehlerbehandlung ist somit keine, sondern eine Selbsttäuschung.

Das wird man spätestens dann merken, wenn man als Entwickler vom Support die Meldung über einen Fehler bekommt und dann das Log-File analysieren muss. Ich wette das viele Fehler nicht per Log-File gefunden werden können.

Was ist zu tun?

Im ersten Schritt sollten alle throw ex Aufrufe durch ein einfaches throw ersetzt werden. Das führt dazu, dass die aufgetretene Exception genau so wie sie ist, nach “oben” weitergeleitet wird. Dann sollte das Logging angepasst werden: Bei einem einfachen Try&Catch sollte das Logging – wenn möglich – so geändert werden, dass die Exception an den Logger übergeben wird (Die meisten Logger sollten das Unterstützen):

try
{
    [do some errors]
}
Catch (Exception ex)
{
    log.Error(ex);
    throw;
}

Im Fall der Verschachtelten Try&Catches wird nun noch an jeder Stelle in das Log geschrieben. Das ist nicht sinnvoll, das dadurch das Log sehr groß werden kann und die Informationen mehrfach im Log vorhanden sind. Es sollte also nur an einer Stelle geloggt werden und zwar am besten ganz oben, beim letzten Try&Catch.

In ASP.NET kann das sogar in der global.asax bei Application_Error gemacht werden.

Die verschachtelten TryCatches sehen dann logisch wie folgt aus:

try
{
    try
    {
        try
       {
           try
           {
                [do some errors]
           }
           Catch (Exception ex)
           {
                throw;
           }
        }
        Catch (Exception ex)
        {
           throw;
        }
    }
    Catch (Exception ex)
    {
        throw;
    }
}
Catch (Exception ex)
{
    log.Error(ex);
    HandleTheException();
}

Im letzten Catch wird dann die eigentliche Fehlerbehandlung gemacht: Es wird dem Benutzer eine sinnvolle Meldung ausgegeben (Nicht die Fehlermeldung, mit der kann der Benutzer nichts anfangen!)

Nach diesem Refactoring, sollte man sich  fragen welche Try&Catches dann noch Sinn machen, wen nichts anderes Passiert, als die Exceptions durchzureichen. Ich bin ganz sicher, dass man einige nun Entfernen könnte.

Generell sollte man nicht grundlos alle möglichen Fehler abfangen, sondern in erster Linie Fehler vermeiden. Natürlich gibt es Stellen, bei denen man Fehler nicht Verhindern kann, das ist immer dann der Fall, wenn man aus seiner Anwendung raus auf etwas zugreift: Datenbank und Dateizugriffe, Web- und Webservice-Zugriffe, COM, etc.

Auf diese Art hat man nun schon mal eine relativ saubere Fehlerbehandlung.

Aber es geht immer noch besser:

An Stellen, an denen mögliche Fehler nicht verhindert werden können, können diese abgefangen und mit einer eigenen Exception nach “oben” weitergeben werden. Die ursprüngliche Exception wird dabei als InnerException mitgegeben:

Catch (Exception ex)
{
    throw new MyVeryOwnHttpException(ex)
}

So kann man in der Darstellungsschicht individuell auf die Fehler reagieren und dem Benutzer einen entsprechenden Hinweis geben:

Catch (MyVeryOwnHttpException ex)
{
    log.Error(ex.InnerException);
    HandleTheVeryOwnHttpException(“Die Verbindung zum Server konnte nicht hergestellt werden.\n Bitte prüfen Sie ob Sie mit dem Internet Verbunden sind.\nUnd so weiter und so fort”, ex);
}

Man kann so spezielle Arten von Ausnahmen zusammenfassen und entsprechend drauf reagieren. Es ist dem Anwender in der Regel egal, was der genaue Fehler ist. Ob das jetzt ein Verbindungsfehler ist oder der Server einen Fehler zurückgibt ist egal. Wichtig ist, dass in diesem Fall die Anwendung nicht mit dem Server kommunizieren kann.

In der Log-File steht auf jeden Fall der konkrete Fehler mit dem kompletten StackTrace und die Fehlersuche wird dadurch wesentlich vereinfacht.

DotNetKicks-DE Image
Posted: Donnerstag, 19. August 2010 10:42 von Jürgen Gutsch

Kommentare

dotnet-kicks.de sagte:

Sie wurden gekickt (eine gute Sache) - Trackback von dotnet-kicks.de

# August 19, 2010 11:03

Peter Bucher sagte:

Salute Jürgen

Guter Beitrag.

Zu Punkt zwei hätte ich noch eine Ergänzung:

Nicht nur der Stacktrace wird mit "throw ex" verworfen, sondern _auch_ die InnerException!

Gruss Peter

# August 19, 2010 13:33

kristof sagte:

Sehr fein, dass jemand das Kind mal beim Namen nennt...

Der Vollständigkeit halber sollte hier unbedingt auf die Enterprise Library 5.0 und den darin enthaltenen Exception Handling Application Block hingeweisen werden: http://bit.ly/cd3mTF.

Etwas hakelig zu implementieren, wenn man Spezialanforderungen hat, aber dafür mit dem Logging Application Block das flexibelste und sicherste in Sachen Error-Handling was ich kenne.

Kristof

# August 19, 2010 13:58

Jürgen Gutsch sagte:

@Peter: stimmt, du hast absolut recht. Werde das noch Anfügen. Vielen Dank :-)

@Christof: Vielen Dank für den Tipp. Zusätzlich zu den Enterprise Library 5.0, gibt es noch ELMAH (http://code.google.com/p/elmah/) als weitere Unterstützung. Beide habe ich nicht ausprobiert, aber ELMAH soll etwas einfacher in der Handhabung sein.

# August 19, 2010 14:13

Peter Bucher sagte:

Salute zusammen

ELMAH ist nur für Webanwendungen nutzbar, dort aber sehr gut.

Eigentlich müsste man ELMAH mal im deutschsprachingen Raum pushen.

Meines Wissens ist das da noch relativ unbekannt.

Gruss Peter

# August 19, 2010 16:59

frank1234 sagte:

Hallo,

ist eigentlich

Try

{

[some code]

}

Catch (Exception ex)

{

   throw;

}

nicht redundant?

Macht

[some code]

nicht genau das gleiche?

# August 20, 2010 13:56

Jürgen Gutsch sagte:

Hallo Frank,

ganz genau :-)

Das hatte ich oben geschrieben. Die meisten Try&Catches machen so keinen Sinn mehr und kann man grad weglassen ;-)

Oft ist weniger mehr :-)

Bei Transaktionen machen auch solche Try&Catches auf alle Fälle noch einen Sinn, um im Catch die Tranaktion zurücksetzen zu können.

# August 20, 2010 14:31

frank1234 sagte:

Hallo Jürgen,

wenn ich folgenden Code im Debug und im Release Modus kompiliere bekomme ich unterschiedlichen Output:

using System;

class Program

{

   static void Main()

   {

       try

       {

           First();

       }

       catch (Exception e)

       {

           Console.WriteLine(e);

       }

   }

   static void First()

   {

int nix = 0;

var neu = 13/nix;

   }

}

PS C:\temp\test1> .\TestWithoutCatch.exe

System.DivideByZeroException: Attempted to divide by zero.

  at Program.Main()

PS C:\temp\test1> .\TestWithoutCatchDebug.exe

System.DivideByZeroException: Attempted to divide by zero.

  at Program.First() in c:\temp\test1\TestWithoutCatch.cs:line 18

  at Program.Main() in c:\temp\test1\TestWithoutCatch.cs:line 8

Wie es scheint bekomme ich den ganzen Stack nur im Debug Modus.

Wenn ich aber catch(Exception){throw;} in der Methode First() ergänze, erscheint der volle Stack auch nach einer Release Kompilierung.

Das würde ja bedeuten, dass auch ein "einfacher" catch(Exception){throw;} zumindest bei einer Release Kompilierung nicht redundant ist.

Oder?

# August 20, 2010 15:22

Jürgen Gutsch sagte:

Hallo Frank,

du hast recht. Ich habe mal eine weitere Methode dazwischen geschalten und nur in First() as throw stehen. Der Stack beinhaltet im Release Build nicht die dazwischenliegende Methode. Erste Wenn ich in jeder Methode die Exception abfange und wieder werfe ist der Stack Trace komplett.

Demnach ist es im Release Build nicht redundant!!! Komisch eigentlich...

@All, weis jemand wieso das so ist?

# August 20, 2010 16:50

Thorsten1983 sagte:

Hi Jürgen,

netter Artikel, aber man sollte Exceptions nur mit eigenen Wrappen, wenn man dafür einen Grund hat, so die Best Practise. Immer eine Custom Exception drum machen ist nicht immer sinnvoll.

Gruß Thorsten

# August 21, 2010 15:48

Jürgen Gutsch sagte:

Hi Thorsten,

danke für deinen Kommentar. :-)

Ich bin da etwas anderer Meinung: In vielen Fällen macht das Sinn. Ich muss mal schauen, ob ich das ineinem Separaten Beitrag gut begründen kann. Aber es kommt darauf an, wo mach sich gerade bewegt:

Wenn du eine API oder ein Framework für andere schreibst, hast du IMO Recht.

# August 23, 2010 06:31
Anonyme Kommentare sind nicht zugelassen