Try / Catch / Log / Rethrow - Ist Anti-Pattern?


18

Ich kann mehrere Posts sehen, in denen die Bedeutung der Behandlung von Ausnahmen an zentraler Stelle oder an Prozessgrenzen als gute Praxis hervorgehoben wurde, anstatt jeden Codeblock um try / catch herum zu verunreinigen. Ich bin der festen Überzeugung, dass die meisten von uns die Wichtigkeit verstehen, aber ich sehe, dass die Leute immer noch mit Catch-Log-Rethrow-Anti-Pattern enden, hauptsächlich, weil sie, um die Fehlerbehebung in Ausnahmefällen zu vereinfachen, mehr kontextspezifische Informationen protokollieren möchten (Beispiel: Methodenparameter) übergeben) und der Weg ist, die Methode um try / catch / log / rethrow zu wickeln.

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

Gibt es einen richtigen Weg, um dies zu erreichen und gleichzeitig die bewährten Verfahren für die Ausnahmebehandlung beizubehalten? Ich habe dafür von AOP-Frameworks wie PostSharp gehört, möchte aber wissen, ob mit diesen AOP-Frameworks Nachteile oder erhebliche Leistungskosten verbunden sind.

Vielen Dank!


6
Es gibt einen großen Unterschied zwischen dem Umbrechen jeder Methode in try / catch, dem Protokollieren der Ausnahme und dem Mitschleppenlassen des Codes. Und versuchen / fangen und werfen Sie die Ausnahme mit zusätzlichen Informationen. Erstens ist schreckliche Praxis. Zweitens ist dies ein guter Weg, um Ihre Debug-Erfahrung zu verbessern.
Euphoric

Ich sage "try / catch" für jede Methode und logge mich einfach in den catch-Block ein und wieder ein - ist das in Ordnung?
rahulaga_dev

2
Wie von Amon betont. Wenn Ihre Sprache Stack-Traces enthält, ist es sinnlos, jeden Fang einzuloggen. Das Einschließen der Ausnahme und das Hinzufügen zusätzlicher Informationen ist jedoch eine bewährte Methode.
Euphoric

1
Siehe @ Liaths Antwort. Jede Antwort, die ich gab, würde so gut wie seine Antwort widerspiegeln: Fang Ausnahmen so früh wie möglich ab und wenn alles, was du in diesem Stadium tun kannst, ist, einige nützliche Informationen zu protokollieren, dann tue dies und wirf es erneut. Es ist meiner Ansicht nach unsinnig, dies als Antimuster zu betrachten.
David Arno

1
Liath: kleines Code-Snippet hinzugefügt. Ich benutze c #
rahulaga_dev

Antworten:


18

Das Problem ist nicht der lokale Catch-Block, das Problem ist das Protokoll und der Rethrow . Behandeln Sie die Ausnahmebedingung oder schließen Sie sie mit einer neuen Ausnahmebedingung ab, die zusätzlichen Kontext hinzufügt, und lösen Sie diese aus. Andernfalls werden Sie für dieselbe Ausnahme auf mehrere doppelte Protokolleinträge stoßen.

Die Idee hier ist, die Fähigkeit zu verbessern , Ihre Anwendung zu debuggen.

Beispiel 1: Behandeln Sie es

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

Wenn Sie die Ausnahme behandeln, können Sie die Wichtigkeit des Ausnahmeprotokolleintrags leicht herabstufen, und es gibt keinen Grund, diese Ausnahme in der gesamten Kette durchzulaufen. Es ist schon erledigt.

Das Behandeln einer Ausnahme kann das Informieren von Benutzern über ein Problem, das Protokollieren des Ereignisses oder das einfache Ignorieren des Ereignisses umfassen.

HINWEIS: Wenn Sie eine Ausnahme absichtlich ignorieren, empfehle ich, in der leeren catch-Klausel einen Kommentar anzugeben, der klar angibt, warum. Dies lässt zukünftige Betreuer wissen, dass es sich nicht um einen Fehler oder eine verzögerte Programmierung handelte. Beispiel:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

Beispiel 2: Füge zusätzlichen Kontext hinzu und wirf

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

Das Hinzufügen von zusätzlichem Kontext (wie Zeilennummer und Dateiname im Parsing-Code) kann dazu beitragen, das Debuggen von Eingabedateien zu verbessern - vorausgesetzt, das Problem ist vorhanden. Dies ist eine Art Sonderfall. Wenn Sie eine Ausnahme in eine "ApplicationException" umbrechen, um sie umzubenennen, hilft dies nicht beim Debuggen. Stellen Sie sicher, dass Sie zusätzliche Informationen hinzufügen.

Beispiel # 3: Mach nichts mit der Ausnahme

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

In diesem letzten Fall lassen Sie die Ausnahme einfach zu, ohne sie zu berühren. Der Ausnahmebehandler auf der äußersten Ebene kann die Protokollierung durchführen. Die finallyKlausel wird verwendet, um sicherzustellen, dass alle von Ihrer Methode benötigten Ressourcen bereinigt werden. Hier kann jedoch nicht protokolliert werden, dass die Ausnahme ausgelöst wurde.


Ich mochte " Das Problem ist nicht der lokale catch-Block, das Problem ist das log und rethrow ". Aber irgendwann bedeutet es auch OK zu sein, dass try / catch auf alle Methoden verteilt ist, nicht wahr? Ich denke, es muss eine Richtlinie geben, die sicherstellt, dass diese Praxis vernünftig befolgt wird, anstatt dass jede Methode dies tut.
rahulaga_dev

Ich habe in meiner Antwort einen Leitfaden angegeben. Beantwortet dies nicht Ihre Fragen?
Berin Loritsch

@rahulaga_dev Ich glaube nicht, dass es einen Leitfaden / eine Silberkugel gibt, also löse dieses Problem, da es stark vom Kontext abhängt. Es kann keine allgemeine Richtlinie geben, die angibt, wo eine Ausnahme zu behandeln ist oder wann sie erneut ausgelöst werden muss. IMO ist die einzige Richtlinie, die ich sehe, die Protokollierung / Verarbeitung auf den spätestmöglichen Zeitpunkt zu verschieben und die Protokollierung in wiederverwendbarem Code zu vermeiden, damit Sie keine unnötigen Abhängigkeiten erstellen. Benutzer Ihres Codes wären nicht zu amüsiert, wenn Sie Dinge protokollieren würden (dh Ausnahmen behandeln würden), ohne ihnen die Möglichkeit zu geben, sie auf ihre eigene Weise zu behandeln. Nur meine zwei Cent :)
andreee

7

Ich glaube nicht, dass lokale Fänge ein Anti-Pattern sind. Wenn ich mich richtig erinnere, wird es tatsächlich in Java erzwungen!

Was für mich bei der Implementierung der Fehlerbehandlung ausschlaggebend ist, ist die Gesamtstrategie. Möglicherweise möchten Sie einen Filter, der alle Ausnahmen an der Servicegrenze abfängt, und Sie möchten sie möglicherweise manuell abfangen - beides ist in Ordnung, solange eine Gesamtstrategie vorliegt, die den Codierungsstandards Ihres Teams entspricht.

Persönlich fange ich gerne Fehler in einer Funktion ab, wenn ich eine der folgenden Möglichkeiten habe:

  • Kontextinformationen hinzufügen (z. B. den Status von Objekten oder den aktuellen Status)
  • Behandeln Sie die Ausnahme sicher (z. B. eine TryX-Methode)
  • Ihr System überschreitet eine Servicegrenze und ruft eine externe Bibliothek oder API auf
  • Sie möchten eine andere Art von Ausnahme abfangen und erneut auslösen (möglicherweise mit dem Original als innerer Ausnahme)
  • Die Ausnahme wurde als Teil einer Hintergrundfunktionalität mit geringem Wert ausgelöst

Wenn es nicht einer dieser Fälle ist, füge ich kein lokales try / catch hinzu. Wenn dies der Fall ist, kann ich je nach Szenario die Ausnahme behandeln (z. B. eine TryX-Methode, die false zurückgibt) oder erneut auslösen, damit die Ausnahme von der globalen Strategie behandelt wird.

Beispielsweise:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

Oder ein Rethrow-Beispiel:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

Anschließend wird der Fehler oben im Stapel abgefangen und dem Benutzer eine benutzerfreundliche Meldung angezeigt.

Welchen Ansatz Sie auch wählen, es lohnt sich immer, Komponententests für diese Szenarien zu erstellen, damit Sie sicherstellen können, dass sich die Funktionalität nicht ändert, und den Projektfluss zu einem späteren Zeitpunkt stören.

Sie haben nicht erwähnt, in welcher Sprache Sie arbeiten, sind aber ein .NET-Entwickler und haben dies zu oft gesehen, um es nicht zu erwähnen.

SCHREIBE NICHT:

catch(Exception ex)
{
  throw ex;
}

Verwenden:

catch(Exception ex)
{
  throw;
}

Ersteres setzt die Stapelspur zurück und macht Ihren Top-Level-Fang völlig unbrauchbar!

TLDR

Lokales Fangen ist kein Anti-Pattern, es kann oft Teil eines Designs sein und dabei helfen, dem Fehler zusätzlichen Kontext hinzuzufügen.


3
Was bedeutet es, im catch zu protokollieren, wenn derselbe Logger im Top-Level-Exception-Handler verwendet wird?
Euphoric

Möglicherweise verfügen Sie über zusätzliche Informationen (z. B. die lokalen Variablen), auf die Sie oben im Stapel keinen Zugriff haben. Ich werde das Beispiel aktualisieren, um es zu veranschaulichen.
Liath

2
In diesem Fall werfen Sie eine neue Ausnahme mit zusätzlichen Daten und einer inneren Ausnahme.
Euphoric

2
@Euphoric yep, ich habe das auch gesehen, persönlich bin ich jedoch kein Fan, da Sie für fast jede einzelne Methode / jedes einzelne Szenario, das meiner Meinung nach viel Aufwand bedeutet, neue Ausnahmetypen erstellen müssen. Das Hinzufügen der Protokollzeile hier (und vermutlich oben) hilft dabei, den Codefluss bei der Diagnose von Problemen zu veranschaulichen
Liath

4
Java zwingt Sie nicht, die Ausnahme zu behandeln, es zwingt Sie, sich dessen bewusst zu sein. Sie können es entweder abfangen und tun, was auch immer, oder es einfach als etwas deklarieren, das die Funktion auslösen kann, und nichts damit in der Funktion tun.
Newtopian

4

Das hängt sehr von der Sprache ab. C ++ bietet beispielsweise keine Stack-Traces in Ausnahmefehlermeldungen. Daher kann es hilfreich sein, die Ausnahme durch häufiges Abfangen und erneutes Auslösen von Protokollen zu verfolgen. Im Gegensatz dazu bieten Java und ähnliche Sprachen sehr gute Stack-Traces, obwohl das Format dieser Stack-Traces möglicherweise nicht sehr konfigurierbar ist. Das Abfangen und Wiederherstellen von Ausnahmen in diesen Sprachen ist völlig sinnlos, es sei denn, Sie können wirklich einen wichtigen Kontext hinzufügen (z. B. das Verbinden einer einfachen SQL-Ausnahme mit dem Kontext einer Geschäftslogikoperation).

Jede Fehlerbehandlungsstrategie, die durch Reflektion implementiert wird, ist fast notwendigerweise weniger effizient als die in die Sprache integrierte Funktionalität. Darüber hinaus verursacht die allgegenwärtige Protokollierung unvermeidbaren Leistungsaufwand. Sie müssen also den Informationsfluss, den Sie erhalten, gegen andere Anforderungen für diese Software abwägen. Lösungen wie PostSharp, die auf Instrumenten auf Compilerebene basieren, sind im Allgemeinen viel besser als die Laufzeitreflexion.

Ich persönlich glaube, dass es nicht hilfreich ist, alles aufzuzeichnen, da es eine Menge irrelevanter Informationen enthält. Ich bin daher skeptisch gegenüber automatisierten Lösungen. Bei einem guten Protokollierungsrahmen kann es ausreichen, eine vereinbarte Codierungsrichtlinie zu haben, in der erläutert wird, welche Art von Informationen Sie protokollieren möchten und wie diese Informationen formatiert werden sollten. Sie können dann die Protokollierung dort hinzufügen, wo es darauf ankommt.

Die Anmeldung an der Geschäftslogik ist viel wichtiger als die Anmeldung an den Dienstprogrammfunktionen. Durch das Sammeln von Stack-Traces realer Absturzberichte (die nur auf der obersten Ebene eines Prozesses protokolliert werden müssen) können Sie Bereiche des Codes ermitteln, in denen die Protokollierung den größten Wert hätte.


4

Wenn ich try/catch/login jeder Methode sehe , wirft es Bedenken auf, dass die Entwickler keine Ahnung hatten, was in ihrer Anwendung passieren könnte oder nicht, das Schlimmste vermuteten und alles wegen all der erwarteten Fehler präventiv überall protokollierten.

Dies ist ein Symptom dafür, dass Unit- und Integrationstests unzureichend sind und Entwickler daran gewöhnt sind, viel Code im Debugger durchzuarbeiten und hoffen, dass sie durch viel Protokollierung in der Lage sind, fehlerhaften Code in einer Testumgebung bereitzustellen und die Probleme zu finden, indem sie nachsehen die Protokolle.

Code, der Ausnahmen auslöst, kann nützlicher sein als redundanter Code, der Ausnahmen abfängt und protokolliert. Wenn Sie eine Ausnahme mit einer aussagekräftigen Meldung auslösen, wenn eine Methode ein unerwartetes Argument empfängt (und an der Servicegrenze protokolliert), ist es weitaus hilfreicher, die als Nebenwirkung des ungültigen Arguments ausgelöste Ausnahme sofort zu protokollieren und zu erraten, was sie verursacht hat .

Nullen sind ein Beispiel. Wenn Sie als Argument oder Ergebnis eines Methodenaufrufs einen Wert erhalten, der nicht null sein sollte, lösen Sie die Ausnahme aus. Protokollieren Sie die resultierenden NullReferenceExceptiongeworfenen fünf Zeilen später nicht nur wegen des Nullwerts. In beiden Fällen erhalten Sie eine Ausnahme, aber eine sagt Ihnen etwas, während die andere Sie dazu bringt, nach etwas zu suchen.

Wie von anderen gesagt, ist es am besten, Ausnahmen an der Dienstgrenze zu protokollieren oder wenn eine Ausnahme nicht erneut ausgelöst wird, weil sie ordnungsgemäß behandelt wird. Der wichtigste Unterschied ist zwischen etwas und nichts. Wenn Ihre Ausnahmen an einer Stelle protokolliert sind, die leicht zu erreichen ist, finden Sie die Informationen, die Sie benötigen, wenn Sie sie benötigen.


Vielen Dank, Scott. Der von Ihnen gemachte Punkt " Wenn Sie eine Ausnahme mit einer aussagekräftigen Meldung auslösen, wenn eine Methode ein unerwartetes Argument empfängt (und es an der Dienstgrenze protokolliert) " hat mir wirklich geholfen, die Situation zu visualisieren, die im Kontext von Methodenargumenten um mich herum schwebt. Ich halte es in diesem Fall für sinnvoll, eine Schutzklausel zu haben und ArgumentException
auszulösen

Scott, ich habe das gleiche Gefühl. Immer wenn ich sehe, dass der Kontext protokolliert und neu protokolliert wird, habe ich das Gefühl, dass Entwickler die Invarianten der Klasse nicht kontrollieren oder den Methodenaufruf nicht schützen können. Stattdessen wird jede Methode bis zum Ende in ein ähnliches try / catch / log / throw eingebunden. Und es ist einfach schrecklich.
Max

2

Wenn Sie Kontextinformationen aufzeichnen müssen, die nicht bereits in der Ausnahme enthalten sind, verpacken Sie sie in eine neue Ausnahme und geben die ursprüngliche Ausnahme als an InnerException. Auf diese Weise bleibt die ursprüngliche Stapelspur erhalten. So:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Der zweite Parameter für den ExceptionKonstruktor enthält eine innere Ausnahme. Dann können Sie alle Ausnahmen an einem einzigen Ort protokollieren, und Sie erhalten weiterhin die vollständige Stapelverfolgung und die Kontextinformationen in demselben Protokolleintrag.

Möglicherweise möchten Sie eine benutzerdefinierte Ausnahmeklasse verwenden, aber der Punkt ist der gleiche.

try / catch / log / rethrow ist ein Durcheinander, weil es zu verwirrenden Protokollen führt - was ist, wenn zwischen dem Protokollieren der Kontextinformationen und dem Protokollieren der tatsächlichen Ausnahme im Top-Level-Handler in einem anderen Thread eine andere Ausnahme auftritt? try / catch / throw ist jedoch in Ordnung, wenn die neue Ausnahme dem Original Informationen hinzufügt.


Was ist mit dem ursprünglichen Ausnahmetyp? Wenn wir wickeln, ist es weg. Ist es ein Problem? Jemand hat sich zum Beispiel auf die SqlTimeoutException verlassen.
Max

@Max: Der ursprüngliche Ausnahmetyp ist weiterhin als innere Ausnahme verfügbar.
JacquesB

Das ist, was ich meine! Jetzt wird jeder in der Aufrufliste, der nach der SqlException gesucht hat, sie niemals bekommen.
Max

1

Die Ausnahme selbst sollte alle Informationen enthalten, die für eine ordnungsgemäße Protokollierung erforderlich sind, einschließlich Meldung, Fehlercode und was nicht. Daher sollte es nicht erforderlich sein, eine Ausnahme nur zum erneuten Auslösen oder Auslösen einer anderen Ausnahme abzufangen.

Häufig wird das Muster mehrerer Ausnahmen, die abgefangen und erneut ausgelöst wurden, als allgemeine Ausnahme angezeigt, z. B. das Abfangen einer DatabaseConnectionException, einer InvalidQueryException und einer InvalidSQLParameterException sowie das erneute Auslösen einer DatabaseException. Trotzdem würde ich argumentieren, dass all diese spezifischen Ausnahmen in erster Linie von DatabaseException herrühren sollten, so dass ein erneutes Werfen nicht erforderlich ist.

Sie werden feststellen, dass das Entfernen unnötiger Try-Catch-Klauseln (auch für reine Protokollierungszwecke) die Arbeit tatsächlich erleichtert und nicht erschwert. Nur die Stellen in Ihrem Programm, an denen die Ausnahme behandelt wird, sollten die Ausnahme protokollieren. Andernfalls sollte ein programmweiter Ausnahmebehandler für einen letzten Versuch, die Ausnahme zu protokollieren, fehlschlagen, bevor das Programm ordnungsgemäß beendet wird. Die Ausnahme sollte einen vollständigen Stack-Trace haben, der den genauen Punkt angibt, an dem die Ausnahme ausgelöst wurde. Daher ist es häufig nicht erforderlich, eine "Kontext" -Protokollierung bereitzustellen.

Das besagte AOP kann eine schnelle Lösung für Sie sein, obwohl es normalerweise eine leichte Verlangsamung insgesamt mit sich bringt. Ich würde Sie ermutigen, stattdessen die unnötigen try catch-Klauseln vollständig zu entfernen, wenn nichts von Wert hinzugefügt wird.


1
" Die Ausnahme selbst sollte alle Informationen enthalten, die für eine ordnungsgemäße Protokollierung erforderlich sind, einschließlich Meldung, Fehlercode und was nicht. " In der Praxis sollten sie jedoch keine Nullreferenzausnahmen enthalten, was ein klassischer Fall ist. Mir ist keine Sprache bekannt, die Ihnen die Variable angibt, die sie beispielsweise in einem komplexen Ausdruck verursacht hat.
David Arno

1
@DavidArno Stimmt, aber jeder Kontext, den Sie bereitstellen könnten, könnte auch nicht so spezifisch sein. Sonst hättest du try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }. Die Stapelverfolgung ist in den meisten Fällen ausreichend, aber da dies nicht der Fall ist, ist die Art des Fehlers in der Regel ausreichend.
Neil
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.