Zwei Fragen bleiben: Wann ist eine Ausnahme überhaupt das richtige Mittel — und wer fängt sie am Ende?
Ausnahmen sind für das Aussergewöhnliche
Der Name ist Programm. Eine Ausnahme beschreibt einen Zustand, mit dem der Aufrufer nicht rechnen musste. Ein Zustand, mit dem er rechnen muss, ist ein normaler Rückgabewert.
Benutzereingabe prüfen
Ausnahme als Ablaufsteuerung
foreach (var zeile in datei)
{
try
{
var zahl = int.Parse(zeile);
summe += zahl;
}
catch (FormatException)
{
ungueltig++;
}
}
// Bei 100 000 Zeilen mit
// vielen Fehlern: spürbar langsamErwarteter Fall
foreach (var zeile in datei)
{
if (int.TryParse(zeile, out var zahl))
summe += zahl;
else
ungueltig++;
}
// Kein Stackaufbau, keine AusnahmeUngültige Zeilen in einer Importdatei sind erwartbar — das ist kein Ausnahmefall, sondern ein Ergebnis. Eine Ausnahme kostet ausserdem ein Vielfaches eines normalen Rückgabewerts.
Eigene Ausnahmetypen — sparsam, aber gezielt
Ein eigener Typ lohnt sich genau dann, wenn ein Aufrufer gezielt darauf reagieren können soll:
public sealed class ZahlungAbgelehntException : Exception
{
public ZahlungAbgelehntException(string belegNummer, string grund)
: base($"Zahlung für Beleg {belegNummer} abgelehnt: {grund}")
{
BelegNummer = belegNummer;
Grund = grund;
}
public string BelegNummer { get; }
public string Grund { get; }
}Kontext gehört in die Ausnahme, nicht ins Log
Die Belegnummer als Eigenschaft — nicht nur im Meldungstext. So kann ein Aufrufer damit arbeiten (den Beleg markieren, gezielt wiederholen), statt die Meldung parsen zu müssen. Eine Ausnahme ohne Kontext zwingt den Empfänger zum Raten.
Und wenn du eine Ausnahme in eine andere übersetzt, gib die ursprüngliche als innerException mit — sonst geht genau die Information verloren, die den Fehler erklärt:
catch (SqlException ex)
{
throw new BelegNichtLesbarException(belegNummer, ex); // ← Ursache erhalten
}Wo gefangen wird
Die Antwort in einer modernen Anwendung ist meistens: an genau einer Stelle, ganz aussen.
Jede Methode mit eigenem try/catch auszustatten führt zu vierzig Fangstellen, die alle dasselbe tun — protokollieren und weiterwerfen. In ASP.NET Core übernimmt das ein IExceptionHandler:
public sealed class ZahlungsFehlerHandler : IExceptionHandler
{
private readonly ILogger<ZahlungsFehlerHandler> _logger;
public ZahlungsFehlerHandler(ILogger<ZahlungsFehlerHandler> logger) => _logger = logger;
public async ValueTask<bool> TryHandleAsync(
HttpContext kontext, Exception ausnahme, CancellationToken token)
{
if (ausnahme is not ZahlungAbgelehntException abgelehnt)
return false; // nicht meiner — der nächste Handler
_logger.LogWarning(abgelehnt, "Zahlung abgelehnt für {Beleg}", abgelehnt.BelegNummer);
await Results
.Problem(title: "Zahlung abgelehnt", detail: abgelehnt.Grund, statusCode: 402)
.ExecuteAsync(kontext);
return true;
}
}
// Program.cs
builder.Services.AddExceptionHandler<ZahlungsFehlerHandler>();
app.UseExceptionHandler();Was nie nach aussen darf
Ein Stacktrace, ein SQL-Text oder ein Verbindungsstring in einer HTTP-Antwort ist ein Sicherheitsproblem. Der Benutzer bekommt eine verständliche Meldung und eine Korrelations-ID; die Einzelheiten gehen ins Log. app.UseExceptionHandler() ohne eigenen Handler tut das in der Produktion bereits richtig.
Und wenn Fehlschlagen normal ist?
Für Fälle, in denen ein Misserfolg zum Alltag gehört — Validierung, Formulare, Importzeilen — gibt es die Alternative, das Ergebnis im Rückgabetyp auszudrücken:
public readonly record struct Ergebnis<T>(bool Erfolg, T? Wert, string? Fehler)
{
public static Ergebnis<T> Ok(T wert) => new(true, wert, null);
public static Ergebnis<T> Fehlgeschlagen(string fehler) => new(false, default, fehler);
}
var ergebnis = _validierung.Pruefe(formular);
if (!ergebnis.Erfolg)
return BadRequest(ergebnis.Fehler);Der Vorteil: Der Misserfolg steht in der Signatur, der Compiler zwingt zur Behandlung, und es entsteht kein Stackaufbau. Der Preis: Das Ergebnis muss durch jede Ebene weitergereicht werden, während eine Ausnahme von selbst nach oben findet.
Die brauchbare Aufteilung: Result für erwartbares Scheitern innerhalb einer Ebene, Ausnahmen für das, was wirklich aussergewöhnlich ist. Beides gleichzeitig überall einzuführen macht den Code nicht sauberer, sondern nur uneinheitlich.
Try it: Task 1
Drei Eingaben, drei unterschiedliche Ausgänge — und keiner davon verschwindet.