Error Handling Part 2: Eigene Ausnahmen werfen

Hier ist der zweite Parte der Serie über Fehlerbehandlung:

  1. Grundlagen der Fehlerbehandlung
  2. Eigene Ausnahamen werfen
  3. Guidelines nach David Abrahams
  4. Fehlerbehandlung auf Anwendungsebene

Diesmal geht es darum wann und wie Ausnahmen generiert werden sollen.

Generell gilt: Ausnahmen sollten immer dann geworfen werden, wenn die Anwendung oder das Objekt sonst in einen undefinierten Zustand gerät. Ausnahmen sollen kein Teil des normalen Programmflusses sein. Ein gutes Beispiel wie es NICHT geht liefert uns mal wieder SharePoint: Wenn ich auf ein Objekt in eine Auflistung zugreifen will, das nicht existiert werde ich mit einer ArgumentException oder eine SPException überrascht. Dies führt zu folgenden Funktionen, die bestimmt schon jeder von uns gesehen hat:

private SPUser GetUser(string user)
{
    try
    {
        return this.currentWeb.AllUsers[user];
    }
    catch
    {
        return null;
    }
}

Hier wäre es viel sinnvoller gewesen <NULL> zurückzugeben. Oder das Null-Pattern zu verwenden… Microsoft selber schreibt übrigens auch, dass man das nicht tun soll: http://msdn.microsoft.com/en-us/library/ms173163.aspx:

Things to avoid when throwing exceptions

  • Exceptions should not be used to change the flow of a program as part of ordinary execution. Exceptions should only be used to report and handle error conditions.
  • Exceptions should not be returned as a return value or parameter instead of being thrown.
  • Do not throw System.Exception, System.SystemException, System.NullReferenceException, or System.IndexOutOfRangeException intentionally from your own source code.
  • Do not create exceptions that can be thrown in debug mode but not release mode. To identify run-time errors during the development phase, use Debug Assert instead.

Also wissen wir jetzt, wann wir Exceptions werfen sollen. Die Frage ist jetzt, welche Exception geworfen werden soll.

Standardausnahmen

Generell gilt, dass die Ausnahme, die wir werfen, immer so spezifisch wie möglich sein sollte. Eine System.Exception sollte nie geworfen werden und wird auch von den Alegri Coding Rules mit einem Fehler quittiert.

Für problematische Argumente, die an die aktuelle Funktion übergeben wurden gibt es eine ArgumentException und ihre Derivate ArgumentNullException und ArgumentOutOfRangeException. Wird eine Aktion versucht, die bei dem aktuellen Zustand nicht zulässig ist, dann kann man die InvalidOperationException verwenden.

Ansonsten gibt es nicht mehr viele, die in Frage kommen. Ev. Noch die FormatException oder die InvalidCastException. Alles was nicht in diese Kategories passt, sollte wohl eher als eigene Ausnahme weitergeleitet werden.

Eigene Ausnahmen

In dotnet ist es sehr einfach eigene Ausnahmetypen zu erstellen. Prinzipiell muss nur folgendes gemacht werden:

  1. Der Name des Types muss auf “Exception” enden.
  2. Man muss von System.Exception oder einem abgeleiteten Typ erben.

Trotzdem gibt es eine ganze Menge Dinge, die man falsch machen kann:

  1. Man muss unbedingt das Serializable Attribut setzen.
  2. Alle Konstriktoren der Basisklasse müssen implementiert werde.

Das Ergebnis sieht so aus:

[Serializable]
public class CustomException : Exception
{
    public CustomException() { }
    public CustomException(string message) : base(message) { }
    public CustomException(string message, Exception inner) : base(message, inner) { }
    protected CustomException(
    System.Runtime.Serialization.SerializationInfo info,
    System.Runtime.Serialization.StreamingContext context)
    : base(info, context) { }
}

Über das Exception-Snippet kann man sich das ganze ganz einfach generieren lassen. Zusätzliche Informationen werden eigentlich nie benötigt. Es geht ja bei der Ausnahme hauptsächlich um den Typ.

Hier noch ein Tipp zum Testen der Klasse. Um den Serialisierungskontruktor zu testen muss ich den Accessor verwenden und zusätzlich ein paar Eigenschaften der Klasse SerializationInfo  hinzufügen:

[TestMethod]
[DeploymentItem("MKA.DailySharp.Samples.dll")]
public void CustomExceptionConstructorTest1()
{
    SerializationInfo info = new SerializationInfo(typeof(CustomException), new FormatterConverter());
    info.AddValue("ClassName", string.Empty);
    info.AddValue("Message", string.Empty);
    info.AddValue("InnerException", new ArgumentException());
    info.AddValue("HelpURL", string.Empty);
    info.AddValue("StackTraceString", string.Empty);
    info.AddValue("RemoteStackTraceString", string.Empty);
    info.AddValue("RemoteStackIndex", 0);
    info.AddValue("ExceptionMethod", string.Empty);
    info.AddValue("HResult", 1);
    info.AddValue("Source", string.Empty);
    StreamingContext context = new StreamingContext();
    CustomException_Accessor target = new CustomException_Accessor(info, context);

    Assert.IsNotNull(target);
}

Der Test bringt natürlich nichts außer die Codeabdeckung hoch zu halten. Wer Serialisierung benötigt kann deshalb noch die Funktionalität testen:

[TestMethod]
public void CustomExceptionConstructor_TestSerialization()
{
    Exception ex = new CustomException("Message", new Exception("Inner exception."));
    string exceptionToString = ex.ToString();
    BinaryFormatter bf = new BinaryFormatter();
    using (MemoryStream ms = new MemoryStream())
    {
        bf.Serialize(ms, ex);
        ms.Seek(0, 0);
        ex = (CustomException)bf.Deserialize(ms);
    }

    Assert.AreEqual(exceptionToString, ex.ToString());
}

Aber das nur am Rande. Wann sollen wir jetzt aber überhaupt eigene Ausnahmetypen verwenden? Wieder als erstes mal ein Anti-Pattern – und wieder einmal SharePoint. Eine eigene Ausnahme, die dann Anwendungsweit verwendet wird bringt überhaupt nichts! Oder hat schon mal jemand wirklich sinnvoll auf eine geworfene SPException reagiert? Was soll denn überhaupt eine SPException sein und wann wird sie geworfen? Man weiß es nicht. Der einzige Vorteil ist, dass die Codeanalyse sich nicht darüber beschwert, dass man von System.Exception erbt.

Eigene Ausnahmen sollen helfen, dass Clients unterschiedlich auf Fehler reagieren können.

public void FooBar()
{
    try
    {
       Foo();
       Bar();
    }
    catch (CustomException)
    {
        FixProblem();
    }
    catch (Custom2Exception ex)
    {
        ReportErrorAndContinue(ex);
    }
    catch (Custom3Exception ex)
    {
        ReportErrorAndShutDown(ex);
    }
    catch (Exception ex)
    {
        ReportGenericError(ex);
        throw;
    }
    finally
    {
        CleanUpResources();
    }
}

Je nachdem welcher Art der Fehler ist, kann unterschiedlich darauf reagiert werden.

Ein gutes Beispiel ist eine Utilities-Klasse, die die Parameter einer Anwendung auswertet. Tritt dabei ein Fehler auf, so wird sie eine “ParameterException”. Wenn der Client jetzt eine Konsolenanwendung ist, dann kann er bei dem Fehler gleich die Hilfe anzeigen.

Ein anderes Beispiel ist ein Modul, das automatische URLs für SharePoint SiteCollections vergibt. Es nimmt die zuletzt angelegt Seite, zerlegt sie anhand eines Patterns und zählt eine Zahl hoch. Tritt hier ein Fehler auf, dann wurde vermutlich eine Testseite angelegt. In diesem Fall sollte eine besondere Ausnahme generiert werden, damit das aufrufende Programm anders die nächste URL ermitteln kann.

Keine Ausnahmen als Teil des normalen Programmflusses verwenden.
Keine zu allgemeinen Ausnahmen werfen – immer so spezifisch wie möglich sein.
Eigene Ausnahme immer als Serializable markieren und alle Konstruktoren der Basisklasse implementieren
Immer Ausnahmen werfen, wenn das System sonst in einen undefiniertem Zustand gelänge
Eigene Ausnahmetypen erstellen, wenn Clients auf Fehler (vermutlich) unterschiedlich reagieren wollen.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s