Fehlersuche: Guard Clauses – den Code auf Fehler vorbereiten

Fehlersuche im Code ist zeitaufwendig. Kluge Strategien für Exceptions und Guard Clauses helfen, effizient versteckte Übeltäter aufzudecken.

In Pocket speichern vorlesen Druckansicht 50 Kommentare lesen
Zwei Verkehrsampeln vor wolkigem Abendhimmel.

(Bild: monticello/Shutterstock.com)

Lesezeit: 16 Min.
Von
  • Sergej Tihonov
Inhaltsverzeichnis

Der Großteil der Softwareentwicklung besteht darin, in zahlreichen Iterationen neue Funktionen zu schreiben, sie zu testen und schließlich Fehler oder Unstimmigkeiten zu identifizieren und zu beheben. Bei der Fehlersuche ist es fatal, wenn keine Anhaltspunkte vorhanden sind, die auf die Ursache hinweisen, sodass sich die betroffenen Stellen im Code nicht genau eingrenzen lassen.

Je mehr Zeit zwischen dem Entwickeln und dem Auftreten des Fehlers vergangen ist, desto kniffliger gestaltet sich die Fehlersuche. Es erschwert die Suche zusätzlich, wenn während dieser Zeit viele neue Funktionen hinzugekommen sind.

Häufige Test-Iterationen lindern das Problem der verstrichenen Zeit. Sie helfen, den betroffenen Bereich einzuschränken und die Qualität neuer Funktionen zu verbessern. Für die bestehende Codebasis ist eine effizientere Strategie nötig, um Hinweise auf auftretende Fehler zu erhalten.

Die Strategie besteht darin, das Problem als Kombination von Symptom und Ursache zu verstehen und die Distanz dazwischen zu reduzieren. Das Symptom ist das sichtbare Resultat, das von dem gewünschten Ergebnis abweicht. Die Ursache ist die Stelle, die diese Abweichung einleitet. Alle Funktionszeilen zwischen den beiden Punkten ergeben die Distanz. Ein Stacktrace ist hilfreich, um sie darzustellen.

Das folgende Beispiel einer Divide-By-Zero Exception verdeutlicht das Konzept. Es demonstriert den Umgang mit einem Stacktrace und zeigt die versteckten Herausforderungen auf.

public class Stack
{
  public void Execute()
  {
    int x = 10;
    int y = 0;
    Console.WriteLine(x / y);
  }
}

Der Code zeigt zwei Variablen, von denen die zweite die Zahl 0 aufnimmt. Die anschließende Division führt zu einer Divide-By-Zero Exception. Das Beispiel zeigt den Unterschied zwischen Symptom und Ursache: Das Stacktrace der Fehlermeldung enthält eine Zeile und deutet auf die Division hin:

Unhandled exception. System.DivideByZeroException:
Attempted to divide by zero.
at ExceptionFirst.Stack.Execute() in ... /Stack.cs:line 7

Das ist das Symptom: Etwas verhält sich nicht so, wie es soll. Die Ursache des Problems befindet sich eine Zeile darüber: die zweite Variable mit dem Wert 0. Eine Änderung an dieser Stelle behebt den Fehler. Die Distanz zwischen dem Symptom und der Ursache ist eine Zeile Code und beträgt somit eins.

Folgender Code extrahiert die Division in eine eigene Methode:

public class Stack
{
  public void Execute()
  {
    int x = 10;
    int y = 0;
    Console.WriteLine(Divide(x, y));
  }

  private int Divide(int x, int y)
  {
    return x / y;
  }
}

Es handelt sich um ein Refactoring ohne funktionale Änderungen: Das Programm löst weiterhin die Divide-By-Zero Exception aus. Das Stacktrace ist aber um eine Zeile länger geworden:

System.DivideByZeroException: Attempted to divide by zero.
  at Stack.Divide(Int32 x, Int32 y) in ... /Stack.cs:line 12
  at Stack.Execute() in … /Stack.cs:line 7

An erster Stelle steht erneut die Zeile mit der Division: das Symptom. An zweiter Stelle ist die Codezeile mit dem Aufruf der separaten Funktion zu finden. Die Codezeile zeigt nicht die Ursache, aber die richtige Methode. Eine Zeile darüber steht die Variable mit dem Wert 0: die Ursache des Fehlers. Die Distanz zwischen dem Symptom und der Ursache beträgt zwei Zeilen Code. Das Stacktrace ist weiterhin aussagekräftig und zeigt alle Methoden, die beim Auffinden der Fehlerquelle hilfreich sind.

Folgender Code extrahiert das Setzen der Variablen in eigene Methoden. Das simuliert das Übernehmen von Werten aus einer unbekannten Quelle wie Benutzereingaben, API-Rückgabewerte oder Inhalte einer Datenbank.

public class Stack
{
  public void Execute()
  {
    int x = ReadX();
    int y = ReadY();
    Console.WriteLine(Divide(x, y));
  }

  private int Divide(int x, int y)
  {
    return x / y;
  }

  private int ReadX()
  {
    return 10;
  }

  private int ReadY()
  {
    return 0;
  }
}

Erneut gibt es keine funktionale Änderung, und das Verhalten bleibt dasselbe. Es tritt weiterhin die Divide-By-Zero Exception auf. Obwohl sich die Ausführung geändert hat, bleibt das Stacktrace dasselbe wie zuvor. Woran liegt das und welche Folgen hat es für die Fehlerbehebung?

System.DivideByZeroException: Attempted to divide by zero.
  at Stack.Divide(Int32 x, Int32 y) in ... /Stack.cs:line 12
  at Stack.Execute() in ... /Stack.cs:line 7

Das Stacktrace zeigt weiterhin das Symptom an erster Stelle und den Aufruf der Division in der zweiten Zeile. Das Problem an dieser Stelle besteht darin, dass die Methode ReadY, die den Wert 0 zurückliefert, in dem Stacktrace nicht erscheint. Somit enthält das Stacktrace nicht mehr alle Informationen über den Ablauf der Anwendung, was die Fehlersuche erschwert. An dieser Stelle reicht das Stacktrace allein nicht aus, sondern es ist eine detaillierte Analyse des Quellcodes erforderlich. In Feinarbeit muss man das dynamische Verhalten des Systems durch Logging und Debugging nachbilden.

Warum taucht der Aufruf der Methode ReadY nicht im Stacktrace auf? Beim Einsatz eines Frameworks sind die angezeigten Stacktraces extrem lang, auch wenn im eigenen Code kaum eine Zeile ausgeführt wurde. Das ist trügerisch und kann zu der falschen Annahme verleiten, dass das Stracktrace den gesamten Verlauf eines Aufrufs enthält.

Ein Stacktrace protokolliert aber nicht die aufgerufenen Zeilen oder Methoden, sondern zeigt immer nur die aktuell offenen Methoden an. Das sind die Methoden, die aufgerufen wurden und deren Ausführung noch nicht abgeschlossen ist. Weil Methoden ihrerseits andere Methoden aufrufen, ergibt sich ein Stack. Abgeschlossene Methoden werden vom Stack entfernt und es geht mit der obersten Methode auf dem Stack weiter.

Die Methode ReadY fehlt also im Stacktrace, da ihre Ausführung abgeschlossen ist. Sie befindet sich in der Nähe der letzten Stelle im Stacktrace, und die Ursache lässt sich einfach ermitteln. Es ergibt sich eine Distanz von drei Codezeilen. Das Beispiel zeigt im Kleinen, warum es oft schwer ist, die Ursache eines Problems zu finden. Gleichzeitig demonstriert es anschaulich das Stacktrace-Verhalten. Mit diesem Wissen kann man eine Strategie entwickeln, um Stacktraces effektiver zu nutzen.