Wo sollten catch und Logging in der Ausnahmebehandlung stehen?
· Go Komura · Ausnahmebehandlung, Logging, Fehlerbehandlung, Design, C# / .NET
Bei Code-Reviews zur Ausnahmebehandlung wiederholen sich meist dieselben drei Beobachtungen:
- Die tiefste gemeinsame Funktion fängt
catch (Exception)ab, sodass der Aufrufer nicht unterscheiden kann, ob „keine Daten vorhanden waren“ oder „etwas mittendrin kaputtgegangen ist“ - Bei einem einzigen Vorfall stehen vier Kopien desselben Stack Trace nebeneinander — aus Repository, Service, Controller und dem Handler für unbehandelte Ausnahmen
- Ein Benutzer hat lediglich abgebrochen, trotzdem erscheint ein
Error-Log, in dem sich dann ein wirklich gefährlicher Fehler versteckt
In keinem dieser Fälle ist try / catch schlecht geschrieben. Es fehlt lediglich eine klare Aufteilung: Wo wird abgefangen, wer schreibt das Log, wo wird die Form des Fehlers festgelegt? Ist diese Rollenverteilung nicht geklärt, fügt jede Entwicklerin und jeder Entwickler „sicherheitshalber“ in der eigenen Schicht weitere catch-Blöcke und Logs hinzu — mit dem Ergebnis, dass die eigentliche Ursache am Ende nicht mehr erkennbar ist.
Dieser Artikel richtet sich an Entwicklerinnen und Entwickler, die mit C# / .NET Geschäftsanwendungen oder Web-APIs schreiben, sowie an alle, die entsprechende Design-Reviews durchführen. Er ordnet, wie man die Grenze zum Abfangen von Ausnahmen, den Ort des Primärlogs und die Verantwortung für Wiederherstellungsentscheidungen aufteilt. Wer vorab festlegt, was in welcher Ebene der Aufrufhierarchie geschieht, trifft im Review wie bei der Störungssuche verlässlichere Entscheidungen.
In diesem Artikel verwendete Begriffe
Drei Begriffe tauchen im Text immer wieder auf. Sie werden hier vorab definiert — nicht als Allgemeinbegriffe, sondern in der Bedeutung, die sie in diesem Artikel haben.
| Begriff | Bedeutung in diesem Artikel |
|---|---|
| Fehlereinheit | Die fachlich zusammenhängende Verarbeitungseinheit, die beschreibt, „was einmal fehlgeschlagen ist“. Eine einzelne Bildschirmbedienung, eine HTTP-Anfrage, ein Job, eine Nachricht oder eine Zeile einer CSV-Datei zählen dazu. Diese Einheit taucht sowohl im Log als auch in der Antwort auf |
| Primärlog | Der einmalige Error- oder Critical-Eintrag für einen Fehler. Er enthält die Fehlereinheit sowie Betriebskontext wie requestId, userId oder eine Ziel-ID. Alle übrigen Einträge gelten als unterstützende Logs auf Debug / Information / Warning |
| Ergebnisumwandlung | Das Beenden des fortlaufenden Werfens einer Ausnahme zugunsten eines Rückgabewerts wie eines Result-Typs oder eines DTOs, das einen Fehler abbildet. Damit lässt sich ein erwarteter Fehler in eine Form bringen, die der Aufrufer per Verzweigung behandeln kann |
Inhaltsverzeichnis
- Das Wichtigste zuerst
catch, Logging und Fehlerbehandlung sind unterschiedliche Dinge- 2.1.
catchen - 2.2. Logging
- 2.3. Fehlerbehandlung
- 2.4. Ausnahmen übersetzen
- 2.1.
- Die Entscheidungstabelle zum ersten Nachschlagen
- Was in welcher Ebene der Aufrufhierarchie zu tun ist
- 4.1. Der tiefste Helper / die tiefste Utility / private Methode
- 4.2. Externe I/O-Grenze: Repository / Gateway / SDK-Wrapper
- 4.3. Application Service / UseCase
- 4.4. Grenze zu UI / HTTP / Job / Message
- 4.5. Der letzte Handler für unbehandelte Ausnahmen
- 4.6. Entlang einer einzelnen Aufrufhierarchie betrachtet
- Erwartete Fehler von unerwarteten Ausnahmen trennen
- Wo und wie oft sollte protokolliert werden?
- Häufige Anti-Patterns
- Checkliste für Reviews
- Grobe Übersicht
- Zusammenfassung
- Referenzlinks
- Verwandte Artikel
1. Das Wichtigste zuerst
- Das Prinzip lautet: in tiefen Schichten nicht breit
catchen. Das Abfangen wird zu den Grenzen verlagert, an denen sich eine Fehlereinheit definieren lässt. - Beim Logging gilt als Grundregel: ein Primärlog pro Fehler. Protokolliert jede Schicht dieselbe Ausnahme erneut mit
Error, leidet darunter, wer das Log liest. - Die Aufgabe der tiefsten Schicht ist Aufräumen, lokaler Rollback, das Übersetzen von Ausnahmen und, falls nötig, ein begrenzter Retry. Wird erneut geworfen, schreibt diese Schicht dort normalerweise kein Primärlog.
- Prozessgrenzen — eine Bildschirmbedienung, eine HTTP-Anfrage, ein Job, eine Nachricht — sind meist der natürlichste Ort für das Primärlog.
- Erwartete Fehler werden auf Ebene des jeweiligen Anwendungsfalls in ein Ergebnis umgewandelt. Nicht alles muss weiter als Ausnahme nach oben geworfen werden.
AppDomain.UnhandledException, WPFsDispatcherUnhandledException, WinForms’ThreadException, der Ausnahme-Handler von ASP.NET Core und die abschließende Ausnahmebehandlung des Hosts sind eher letzte Aufzeichnungsstellen als Wiederherstellungspunkte.- Eine
OperationCanceledExceptiondurch Benutzerabbruch oder Shutdown wird normalerweise nicht als Error behandelt. - Im Zweifel in dieser Reihenfolge prüfen:
- Kann an dieser Stelle wirklich entschieden werden?
- Ist hier bekannt, welche Einheit fehlgeschlagen ist?
- Lässt sich der Zustand hier zurücksetzen oder neu aufbauen?
- Wird dieselbe Ausnahme, wenn hier geloggt wird, auch weiter oben noch geloggt?
Kurz gesagt: nicht dort abfangen, wo man es kann, sondern dort, wo man verantwortlich entscheiden kann.
2. catch, Logging und Fehlerbehandlung sind unterschiedliche Dinge
2.1. catchen
catch bedeutet, eine Ausnahme einmal entgegenzunehmen und den Ablauf zu ändern.
Das allein ist jedoch keine Wiederherstellung.
Wenn eine untere Methode zum Beispiel eine Ausnahme empfängt, aber
- nicht weiß, was dem Benutzer angezeigt werden soll
- nicht weiß, ob wegen dieses Fehlers der ganze Bildschirm gestoppt werden soll oder nur diese eine Operation fehlschlagen darf
- nicht weiß, ob der Request oder Job fortgesetzt werden darf,
dann ist diese Stelle meist nicht der richtige Ort für catch.
2.2. Logging
Ein Log hält nicht nur die Tatsache fest, dass „eine Ausnahme aufgetreten ist“, sondern dokumentiert, welche Arbeit fehlgeschlagen ist, damit sich das später nachvollziehen lässt.
Deshalb liegt an einer guten Log-Stelle meist eines der folgenden Dinge vor.
- requestId / traceId
- userId
- orderId / fileId / batchId
- die wievielte Eingabe es war
- welche Bildschirmbedienung es war
- welche Queue, welche Nachricht
Tiefe Helper und gemeinsame Funktionen kennen oft die technischen Details, aber nicht diesen Kontext. Deshalb sind der Ort, der die technischen Details kennt, und der Ort, der den Betriebskontext kennt, häufig nicht derselbe.
2.3. Fehlerbehandlung
Mit Fehlerbehandlung ist hier etwa Folgendes gemeint.
- Eine Fehlermeldung auf dem Bildschirm anzeigen
- Bei HTTP einen 4xx- / 5xx-Status zurückgeben
- Nur diesen einen Eintrag als fehlgeschlagen behandeln und zum nächsten übergehen
- Das betroffene Subsystem neu initialisieren
- Den Prozess beenden und den Neustart dem Supervisor überlassen
- Ressourcen freigeben und sicher aussteigen
Kurz: entscheiden, wie der Fehler aus Sicht des Aufrufers oder der Benutzerin aussieht.
2.4. Ausnahmen übersetzen
In der Praxis gibt es zwischen catch und „Behandeln“ noch eine weitere wichtige Aufgabe:
die Übersetzung.
Lässt man zum Beispiel
HttpRequestExceptionIOExceptionJsonException- datenbanktreiberspezifische Ausnahmen
- herstellerspezifische SDK-Ausnahmen
unverändert bis zur UI oder zum Controller durchsickern, beginnen die oberen Schichten, die internen Belange der unteren Implementierung zu kennen.
An der Grenze übersetzt man sie deshalb in Fehler, die auf dieser Ebene Bedeutung haben, etwa:
- „Verbindung zum Zahlungsdienst konnte nicht hergestellt werden“
- „Das CSV-Format war fehlerhaft“
- „In das Ziel konnte nicht geschrieben werden“
- „Die Antwort des Geräts war ungültig“
Wichtig ist hier, dass Übersetzung und Logging nicht dasselbe sind. Wer nur übersetzt und erneut wirft, schreibt normalerweise noch kein Primärlog.
3. Die Entscheidungstabelle zum ersten Nachschlagen
In diesem Artikel tauchen drei ähnlich aufgebaute Tabellen auf. Da sie unterschiedliche Rollen haben, hier vorab die Abgrenzung.
| Tabelle | Wann nachschlagen | Was sie zeigt |
|---|---|---|
| Entscheidungstabelle in Kapitel 3 | Beim Design. Wenn die Verantwortung jeder Schicht festgelegt wird | Grundhaltung je Ort, ob ein Primärlog geschrieben wird, Hauptverantwortlichkeiten |
| Tabelle der Log-Stellen in Kapitel 6 | Bei der Implementierung. Wenn die Hand beim Schreiben einer Log-Zeile stockt | Für jede Fehlerart, wo mit welcher Stufe protokolliert wird |
| Grobe Übersicht in Kapitel 9 | Beim Review. Zur abschließenden Kontrolle | Kapitel 3 und 6, komprimiert auf die drei Spalten catch / Log / Fehlerbehandlung |
Am einfachsten legt man zunächst mit dieser Tabelle die grobe Richtung fest.
| Ort | Grundhaltung | Primärlog | Hauptverantwortung |
|---|---|---|---|
| Helper / Utility / private Methode | Grundsätzlich nicht breit catchen |
Nein | Aufräumen via finally, lokaler Rollback, minimale Kontextanreicherung |
| Repository / Gateway / SDK-Wrapper | Nur konkrete Ausnahmen abfangen | Normalerweise nein | Übersetzung von Ausnahmen, begrenzter Retry, Verwerfen von Verbindungen und Handles |
| Application Service / UseCase | Erwartete Fehler in Ergebnisse umwandeln | Beim Abfangen hier bei Bedarf | Definition der Fehlereinheit, Behandlung von Teilfehlern, Entscheidungen auf Anwendungsfallebene |
| Grenze UI / Controller / API / Job / Message | Haupt-Abnahmestelle für unerwartete Ausnahmen | Hier meist das Primärlog | Benutzerantworten, HTTP-Antworten, Fortsetzen mit nächstem Eintrag, Abbruchentscheidung |
| Handler für unbehandelte Ausnahmen / letzte Host-Grenze | Letzte Linie gegen Verluste | Critical |
Abschließende Aufzeichnung, Flush, Dump, Exit- / Neustartpfad |
Als Diagramm sieht das ungefähr so aus.
flowchart TD
A["Eine Ausnahme ist aufgetreten"] --> B{"Kann hier über Retry / Ergebnisumwandlung / Fortsetzbarkeit entschieden werden?"}
B -- "Nein" --> C["Grundsätzlich nicht catchen, nach oben weiterreichen"]
B -- "Ja" --> D{"Ist dies eine Schichtgrenze?"}
D -- "Nein" --> E["Nur lokales Aufräumen"]
D -- "Ja" --> F["Bei Bedarf in eine aussagekräftige Ausnahme übersetzen"]
E --> G{"Sind hier Fehlereinheit und Betriebskontext bekannt?"}
F --> G
G -- "Nein" --> H["Kein Primärlog schreiben, nach oben weiterreichen"]
G -- "Ja" --> I["Primärlog einmal schreiben und die Antwort festlegen"]
I --> J["Bei Bedarf: beenden / neu initialisieren / mit nächstem Eintrag fortfahren"]
Das Diagramm macht zwei Punkte deutlich.
- Der erste Grund für
catchist Wiederherstellung oder Aufräumen — nicht das Logging. - Der erste Grund zum Loggen ist, dass der Betriebskontext vorliegt — nicht, dass eine Ausnahme entdeckt wurde.
4. Was in welcher Ebene der Aufrufhierarchie zu tun ist
4.1. Der tiefste Helper / die tiefste Utility / private Methode
Hier gilt grundsätzlich: nicht breit abfangen.
Orte wie Stringkonvertierung, Parsing, Berechnung, interne Formatierung oder gemeinsame Helper können nicht entscheiden,
- welche Bildschirmbedienung es war
- welcher Request es war
- ob es genügt, nur diesmal fehlzuschlagen
- ob der ganze Bildschirm geschlossen werden soll.
Was diese Schicht tun darf, ist im Wesentlichen Folgendes.
- Ressourcen in
finallyfreigeben - Halb veränderten lokalen Zustand zurückrollen
- Der Ausnahmemeldung minimalen Kontext hinzufügen
- Durch einen passenderen Ausnahmetyp ersetzen
- Nicht mehr wiederverwendbare Objekte verwerfen
Gemeinsam ist all dem: Es ist Aufräumarbeit, die korrekt ausgeführt werden kann, ohne den Aufrufer zu kennen. Nur Dinge, die keine Entscheidung erfordern, gehören hierher — alles, was eine Entscheidung braucht, wandert nach oben.
Zu vermeiden sind dagegen Schreibweisen wie diese.
catch (Exception)undnull/false/ ein leeres Array zurückgeben- Hier eine
MessageBoxanzeigen - Hier mit
Errorloggen und dann erneut werfen - „Einfach weitermachen“, obwohl der Zustand nicht wiederhergestellt werden kann
Besonders gefährlich ist das Muster, nach dem teilweisen Verändern des eigenen Zustands fehlzuschlagen und diesen Zustand trotzdem weiterzuverwenden. In diesem Fall gilt entweder: an Ort und Stelle zurücksetzen, wenn möglich, oder das Objekt als nicht mehr verwendbar behandeln.
4.2. Externe I/O-Grenze: Repository / Gateway / SDK-Wrapper
Hier ist der Grund für catch eindeutig.
Denn hier treten die Implementierungsdetails der darunterliegenden Schicht zutage.
- Datenbanktreiber-Ausnahmen
- HTTP-Kommunikationsausnahmen
- Datei-I/O-Ausnahmen
- COM- / P/Invoke- / herstellerspezifische SDK-Ausnahmen
- Ausnahmen von Parsing-Bibliotheken und Serialisierern
Diese Schicht erledigt im Wesentlichen vier Dinge.
-
Konkrete Ausnahmen abfangen Nicht ein breites
Exception, sondern konkrete, aussagekräftige Ausnahmen. -
In einen aussagekräftigen Fehler übersetzen Damit die oberen Schichten die internen Details der unteren nicht direkt kennen müssen.
- Ein lokaler Retry gehört hierher, falls nötig
Die Bedingungen sind allerdings streng:
- Der Fehler ist als vorübergehend bekannt
- Die Operation ist idempotent
- Maximale Versuchsanzahl und Wartezeit sind festgelegt
- Das Verhalten bei endgültigem Fehlschlag ist klar definiert Nur wenn alle vier Punkte zutreffen.
- Defekte Verbindungen und Handles verwerfen „Die Verbindung neu aufbauen“ ist oft sicherer als „mit demselben Objekt weitermachen“.
Die Logging-Strategie hier bleibt stabil, wenn man so denkt.
- Wird nach oben erneut geworfen, schreibt man normalerweise kein Primärlog
- Wird die Ausnahme hier abgefangen und in ein Ergebnis umgewandelt, werden die nötigen Logs und Metriken an dieser Stelle geschrieben
- Einzelne Retry-Versuche bleiben im Bereich
Debug/Information/Warning, nur der endgültige Fehlschlag wird deutlich festgehalten
Diese Schicht ist ein Ort der Übersetzung, in der Regel nicht der Ort der finalen Entscheidung.
4.3. Application Service / UseCase
Hier wird entschieden, „wie diese Arbeit fehlschlägt“.
Zum Beispiel:
- ein Speichervorgang
- eine Bestellbestätigung
- ein CSV-Import
- die Verarbeitung eines Batch-Eintrags
- das Anwenden einer Nachricht
— als Anwendungsfall zusammenhängende Einheiten liegen hier.
Diese Schicht kann Entscheidungen wie diese treffen.
- Ein Validierungsfehler lässt nur diesen Versuch fehlschlagen
NotFoundentspricht einem 404- Ein Verstoß gegen eine Geschäftsregel wartet auf eine Korrektur durch die Benutzerin
- Eine fehlerhafte CSV-Zeile wird mit
Warningprotokolliert, die Verarbeitung läuft weiter - Ein vorübergehender Ausfall eines externen Dienstes lässt den gesamten Vorgang fehlschlagen
- Zwischenergebnisse werden verworfen und von vorn begonnen
Kurz: Hier lässt sich die Fehlereinheit festlegen.
Diese Schicht eignet sich für Aufgaben wie:
- Erwartete Fehler in ein
Resultoder ein Fehler-DTO umwandeln - Teilfehler aggregieren
- Festlegen, wie viele Fehlschläge toleriert werden, bevor abgebrochen wird
- Umwandeln in Fehlercodes oder benutzerseitige Nachrichtenschlüssel
Was diese Schicht dagegen nicht tun sollte, ist zu viel UI-Darstellung oder den Aufbau des HTTP-Antworttexts an sich zu ziehen. Die Trennung gelingt sauberer, wenn diese Schicht nur bis zur Bedeutung auf Ebene des Anwendungsfalls entscheidet und die endgültige Darstellung der Grenzschicht überlässt.
4.4. Grenze zu UI / HTTP / Job / Message
Hier liegt in vielen Anwendungen der Ort für das Primärlog.
Zum Beispiel solche Einheiten:
- ein Klick auf „Speichern“ in WinForms / WPF
- eine HTTP-Anfrage in ASP.NET Core
- eine Nachricht in einem Worker
- ein Eingabeeintrag in einem Batch
- ein Lauf eines geplanten Jobs
Dieser Ort weiß:
- was die Operation war
- wessen Operation es war
- der wievielte Eintrag es war
- welcher Request / Batch / welche Nachricht es war
- was bei einem Fehler an Benutzerin oder Aufrufer zurückgegeben werden soll
Diese fünf Dinge liegen in vielen Anwendungen nur an dieser Stelle gleichzeitig vor. Die darunterliegenden Schichten kennen zwar die technischen Details, aber nicht den Betriebskontext, und bis zum Handler für unbehandelte Ausnahmen ist die Fehlereinheit meist nicht mehr bekannt. Deshalb übernimmt diese Grenze häufig die Rolle,
- unerwartete Ausnahmen hier gesammelt abzufangen
- einmal ein Primärlog mit Kontext zu schreiben
- in einen Fehlerdialog, HTTP 500, Problem Details, einen Job-Fehlschlag, das Fortfahren mit dem nächsten Eintrag und Ähnliches umzuwandeln.
Wichtig an dieser Schicht ist nicht das breite Abfangen selbst, sondern dass definiert ist, was nach dem breiten Abfangen zurückgegeben wird.
Bei Batches oder Queues hilft es, in zwei Stufen zu denken.
- An der Grenze eines einzelnen Eintrags abfangen Entscheiden, ob nur dieser eine Eintrag fehlschlägt und weitergemacht wird
- In der äußeren Schleife nicht breit alles unterdrücken Stirbt die äußere Schleife, sollte eher der gesamte Prozess neu gestartet werden
„Einträge einzeln fehlschlagen lassen und weitermachen“ und „die äußere Schleife bleibt trotz unerwarteter Ausnahme stillschweigend am Leben“ sind völlig verschiedene Dinge.
4.5. Der letzte Handler für unbehandelte Ausnahmen
Dies ist die letzte Verteidigungslinie. Kein magischer Wiederherstellungspunkt.
Typische Beispiele sind:
AppDomain.UnhandledException- WPFs
Application.DispatcherUnhandledException - WinForms’
Application.ThreadException - Die Middleware und Handler zur Ausnahmebehandlung von ASP.NET Core
- Die abschließende Ausnahmebehandlung von Generic Host / Workern /
BackgroundService
Die Hauptverantwortung dieser Schicht beschränkt sich im Wesentlichen auf:
- Das abschließende Log
- Flush
- Ein Pfad zur Dump-Erfassung
- Sichern von Sitzungsinformationen und dem letzten Kontext
- Einrichten von Exitcode und Neustartpfad
Umgekehrt sollte man hiervon nicht zu viel erwarten.
- Ist es bis hierher durchgesickert, liegt meist eine Designlücke weiter oben vor
- Der Zustand kann bereits beschädigt sein
- Locks können gehalten werden, was schwere Verarbeitung hier riskant macht
- Auch wenn ein Fortsetzen scheinbar möglich ist, heißt das nicht, dass es sicher ist
Für .NET gibt es dazu auch praktische Hinweise.
AppDomain.UnhandledExceptionist ein Ereignis zur Benachrichtigung und Aufzeichnung unbehandelter Ausnahmen. Zu viel Wiederherstellungslogik hier hineinzupacken, ist riskant.- WPFs
DispatcherUnhandledExceptionbietet die Möglichkeit, mitHandled = truescheinbar fortzufahren, aber zuerst muss beurteilt werden, ob eine Wiederherstellung überhaupt möglich ist. - Auch WinForms’
ThreadExceptionkann die Anwendung nach der Behandlung in einem unbekannten Zustand zurücklassen. - Die Middleware zur Ausnahmebehandlung von ASP.NET Core muss früh in der Pipeline platziert werden, damit sie nachfolgende Ausnahmen abfangen kann.
- Eine unbehandelte Ausnahme in einem
BackgroundServicewird ab .NET 6 protokolliert und stoppt standardmäßig den Host. Den Host zu stoppen und sich auf die Neustartstrategie zu verlassen, ist manchmal sicherer, als in der äußeren Schleife alles zu unterdrücken.
Besonders in Desktop-Anwendungen gibt es die Möglichkeit, „die unbehandelte Ausnahme abzufangen und fortzufahren“. Aber fortfahren zu können und fortfahren zu dürfen sind zwei verschiedene Dinge.
4.6. Entlang einer einzelnen Aufrufhierarchie betrachtet
Betrachten wir zum Beispiel diesen Ablauf.
flowchart LR
A["Grenze zu UI / Controller / Job"] --> B["Application Service / UseCase"]
B --> C["Domain / Fachlogik"]
C --> D["Repository / Gateway / SDK-Wrapper"]
D --> E["DB / HTTP / Datei / Hersteller-SDK"]
Die Rollen verteilen sich dabei etwa so.
Speichern-Button → SaveOrderUseCase → PaymentGateway → HTTP
PaymentGateway- Fängt Kommunikationsfehler und fehlerhafte Antworten ab
- Übersetzt sie in „Verbindung zum Zahlungsdienst fehlgeschlagen“ bzw. „Antwort des Zahlungsdienstes ungültig“
- Führt einen Retry, falls nötig, hier und unter Bedingungen durch
- Schreibt beim erneuten Werfen normalerweise kein Primärlog
SaveOrderUseCase- Wandelt erwartete Fehler wie eine Zahlungsablehnung in Ergebnisse um
- Behandelt es als „nur diese Bestellbestätigung ist fehlgeschlagen“
- Bringt das Fehlerergebnis in eine Form, die UI oder API leicht zurückgeben können
- UI-Button-Handler / Controller
- Fängt unerwartete Ausnahmen gesammelt ab
- Schreibt das Primärlog mit
orderId,userId,requestId - Wandelt es in einen Dialog oder eine 500- / 503-Antwort um
- Der Handler für unbehandelte Ausnahmen
- Zeichnet nur das auf, was bis hierher durchgesickert ist
- Führt Dumps und das abschließende Flush durch
- Priorisiert den Exit-Pfad, nicht die Wiederherstellung
Mit dieser Aufteilung ergibt sich das Muster: technische Details werden unten gekapselt, Betriebskontext wird oben ergänzt, Entscheidungen fallen an der Grenze.
Was jede Schicht bei diesem Vorfall tatsächlich protokolliert
Konkretisiert man, was jede Schicht bei ein und demselben Fehler tatsächlich schreibt, wird die Aufteilung greifbar. Angenommen wird dieser eine Vorgang: „Der Zahlungsdienst hat zweimal ein Timeout geliefert, beim dritten Versuch war er erfolgreich, danach ist bei der Bestandsreservierung ein gebrochener Grundzustand aufgetreten.“
| Schicht | Geschriebenes Log | Stufe | Beispielmeldung |
|---|---|---|---|
PaymentGateway |
Jeder Retry-Versuch | Warning |
Verbindung zum Zahlungsdienst wird erneut versucht. attempt={Attempt}/{MaxAttempts}, orderId={OrderId} |
PaymentGateway |
Beim Übersetzen und erneuten Werfen | Kein Log | – (Primärlog ist Aufgabe der Grenze) |
SaveOrderUseCase |
Beim Umwandeln eines erwarteten Fehlers in ein Ergebnis | Information |
Zahlung der Bestellung wurde abgelehnt. orderId={OrderId}, reason={DeclineReason} |
SaveOrderUseCase |
Bei unerwarteter Ausnahme | Kein Log | – (wird direkt an die Grenze weitergereicht) |
| UI-Button-Handler / Controller | Primärlog der unerwarteten Ausnahme | Error |
Bestätigung der Bestellung fehlgeschlagen. orderId={OrderId}, userId={UserId} + Ausnahmeobjekt |
| Handler für unbehandelte Ausnahmen | Abschließende Aufzeichnung | Critical |
Prozess wird wegen unbehandelter Ausnahme beendet + Ausnahmeobjekt |
Entscheidend ist: Error erscheint nur in genau einer Zeile. Jeder Retry-Versuch liegt auf Warning, der erwartete Fehler auf Information, sodass eine Suche nach Error diesen Vorfall genau einmal findet.
// Primärlog. Das Ausnahmeobjekt wird als erstes Argument übergeben,
// der Kontext der Fehlereinheit wird benannt mitgegeben
_logger.LogError(ex, "Bestätigung der Bestellung fehlgeschlagen. orderId={OrderId}, userId={UserId}",
orderId, userId);
Wird vergessen, das Ausnahmeobjekt als erstes Argument zu übergeben, wird der Stack Trace nicht aufgezeichnet. Übergibt man wie in _logger.LogError(ex.Message) nur den Text, lässt sich die Ursache später nicht mehr nachvollziehen.
5. Erwartete Fehler von unerwarteten Ausnahmen trennen
Bei diesem Thema ist am wichtigsten, nicht alles gleichermaßen als „Ausnahme“ zu behandeln.
Zunächst diese Unterteilung.
| Fehlerart | Wo zuerst behandelt | Typische Behandlung |
|---|---|---|
| Validierungsmangel | Grenze UseCase / Request | Als Eingabefehler zurückgeben |
NotFound / Conflict |
UseCase / Controller | 404 / 409 oder Bildschirmmeldung |
| Benutzerabbruch / Shutdown | Operationsgrenze | Als Abbruch behandeln. Normalerweise nicht Error |
| Fehlerhafte CSV-Zeile | Zeilengrenze | Mit Warning protokollieren, weiter fortfahren |
| Vorübergehendes Timeout, das letztlich fehlschlägt | I/O-Grenze bis Request-Grenze | Nach Retries als Fehlschlag zurückgeben |
NullReferenceException, gebrochener Grundzustand |
Request- / Job-Grenze | Primärlog und Fehlerantwort |
AccessViolationException, schwerer OutOfMemoryException, Anzeichen für native Speicherverletzung |
Letzte Grenze | Critical, eher beenden |
Erwartete Fehler sind Fehler, die im Design bereits vorab entschieden werden können. Unerwartete Ausnahmen sind Fehler, nach denen fraglich ist, ob dem Zustand noch getraut werden kann.
Allein diese Trennung verringert Vorfälle wie:
NotFoundjedes Mal alsErrorprotokollieren- Benutzerabbruch als Ausfall behandeln
- Einen wirklich gefährlichen gebrochenen Grundzustand als „diesmal nur fehlgeschlagen“ durchgehen lassen
6. Wo und wie oft sollte protokolliert werden?
Beim Log-Design ist wichtiger, wer das Primärlog schreibt, als wo genau catch steht.
Sechs Grundregeln gelten.
- Pro Fehler ein Primärlog auf
Error/Critical - Untere Schichten übernehmen bei Bedarf Übersetzung und Kontextanreicherung
- Die obere Grenze schreibt das Primärlog mit Fehlereinheit und Betriebskontext
- Nur die abfangende Schicht trägt die Aufzeichnungsverantwortung für den abgefangenen Fehler
- Erwartete Fehler werden nicht jedes Mal als
Errorprotokolliert OperationCanceledExceptionwird von gewöhnlichen Fehlerlogs getrennt gehalten
Hier eine grobe Tabelle der Log-Stellen. Während die Tabelle in Kapitel 3 festlegte, „welcher Schicht welche Verantwortung zukommt“, zeigt diese Tabelle, wo für welche Fehlerart welche Aufzeichnungsstufe gilt. Wer beim Implementieren nicht weiß, ob dieser catch-Block loggen soll, findet hier die Antwort.
| Situation | Hauptsächlicher Log-Ort | Richtwert für die Stufe | Anmerkung |
|---|---|---|---|
| Validierungsfehler | Grenze Request / UseCase | Information oder kein Log |
Kein Ausfall, sondern ein vertraglicher Fehler |
| Benutzerabbruch / Shutdown | Operationsgrenze | Debug / Information |
Normalerweise nicht Error |
| Vorübergehender Fehler während des Retry | Schicht, die den Retry besitzt | Debug / Warning |
Vor dem endgültigen Fehlschlag nicht zu viel Lärm |
| Retries ausgeschöpft, fehlgeschlagen | Grenze Request / Job, oder die abfangende Schicht | Warning / Error |
Mit Fehlereinheit aufzeichnen |
| Nur eine Zeile fehlerhaft, wird fortgesetzt | Grenze des Eintrags | Warning |
fileId, rowNumber anfügen |
| Unerwartete Ausnahme lässt den ganzen Request scheitern | Grenze Request / UI / Job | Error |
requestId, userId, entityId anfügen |
| Klasse Prozessende | Grenze der unbehandelten Ausnahme | Critical |
Flush, Dump, Neustartpfad |
In der Praxis sehr häufig ist doppeltes Logging wie dieses.
- Das Repository loggt
Error - Der Service loggt dieselbe Ausnahme mit
Error - Der Controller loggt erneut mit
Error - Der letzte Handler für unbehandelte Ausnahmen loggt zusätzlich
Critical
So entstehen bei einem einzigen Vorfall mehrere Kopien desselben Stack Trace nebeneinander. Wer liest, will nicht vier Kopien desselben Stack Trace, sondern ein Primärlog und bei Bedarf wenige unterstützende Logs.
Anders gesagt gilt als Grundregel: einmal loggen, mit so viel Kontext wie nötig.
7. Häufige Anti-Patterns
Im Folgenden werden Schreibweisen aufgelistet, die in Reviews immer wieder auftauchen. Zu den drei repräsentativsten gibt es minimalen NG- und OK-Code. Der Code setzt C# 10 / .NET 6 oder neuer sowie aktivierte nullable Referenztypen voraus und verwendet System.Text.Json und Microsoft.Extensions.Logging.
7.1. In einer tiefen Schicht catch (Exception) und null / false zurückgeben
Dabei geht die Information über die Ursache leicht verloren. Schlimmer noch: Der Aufrufer kann nicht mehr unterscheiden, ob „wirklich keine Daten vorhanden waren“ oder „etwas mittendrin kaputtgegangen ist“.
// NG: In einer tiefen Schicht breit abfangen und null zurückgeben
private static Order? LoadOrder(string path)
{
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Order>(json);
}
catch (Exception)
{
// Der Aufrufer kann nicht unterscheiden, ob die Datei fehlte,
// das JSON fehlerhaft war oder die Festplatte nicht gelesen werden konnte
return null;
}
}
Diese Schicht kann nicht entscheiden, wie der Fehler behandelt werden soll. Die Entscheidung wird an die Grenze weitergereicht, hier beschränkt man sich auf die Übersetzung in einen aussagekräftigen Fehler.
// Der von dieser Schicht geworfene, aussagekräftige Fehlertyp
public sealed class OrderFileFormatException : Exception
{
public OrderFileFormatException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
// OK: Nur übersetzen, die Entscheidung geht an die obere Grenze
private static Order LoadOrder(string path)
{
string json = File.ReadAllText(path);
try
{
return JsonSerializer.Deserialize<Order>(json)
?? throw new OrderFileFormatException($"Die Bestelldatei ist leer: {path}");
}
catch (JsonException ex)
{
// JsonException, ein Detail der unteren Implementierung, wird hier
// in einen für diese Schicht aussagekräftigen Fehler umgewandelt
throw new OrderFileFormatException($"Das Format der Bestelldatei ist ungültig: {path}", ex);
}
// IOException und UnauthorizedAccessException werden nicht übersetzt,
// sondern unverändert weitergereicht. „Die Datei kann nicht gelesen werden“
// ist kein Fehler, dem diese Schicht Bedeutung hinzufügen kann
}
7.2. In jeder Schicht mit Error loggen und dann erneut werfen
Die häufigste Ursache für doppeltes Logging.
- Untere Schichten übersetzen nur
- Die obere Grenze schreibt das Primärlog
Diese Aufteilung reduziert das Problem erheblich.
// NG: Die untere Schicht loggt und wirft erneut. In der oberen Schicht wird
// dieselbe Ausnahme erneut geloggt, sodass zwei Kopien entstehen
public async Task<Receipt> ChargeAsync(Payment payment, CancellationToken ct)
{
try
{
return await _gateway.ChargeAsync(payment, ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Zahlung fehlgeschlagen");
throw;
}
}
Die untere Schicht übersetzt nur und leitet weiter.
// PaymentGatewayException ist wie OrderFileFormatException ein eigener
// Ausnahmetyp, der ausdrückt, „die Kommunikation mit dem Zahlungsdienst ist fehlgeschlagen“
// OK: Die untere Schicht (PaymentGateway) übersetzt nur. Kein Log
public async Task<Receipt> ChargeAsync(Payment payment, CancellationToken ct)
{
try
{
return await _gateway.ChargeAsync(payment, ct);
}
catch (HttpRequestException ex)
{
throw new PaymentGatewayException(
$"Verbindung zum Zahlungsdienst konnte nicht hergestellt werden. orderId={payment.OrderId}", ex);
}
}
An der Grenze, an der Fehlereinheit und Betriebskontext zusammenkommen, wird dann genau einmal das Primärlog geschrieben.
// OK: An der Grenze wird einmal das Primärlog geschrieben und die Antwort an den Aufrufer festgelegt
[ApiController]
public sealed class PaymentController : ControllerBase
{
private readonly ILogger<PaymentController> _logger;
private readonly SaveOrderUseCase _useCase;
public PaymentController(ILogger<PaymentController> logger, SaveOrderUseCase useCase)
{
_logger = logger;
_useCase = useCase;
}
[HttpPost("orders/{orderId}/pay")]
public async Task<IActionResult> PayAsync(string orderId, CancellationToken ct)
{
try
{
Receipt receipt = await _useCase.ExecuteAsync(orderId, ct);
return Ok(receipt);
}
catch (PaymentGatewayException ex)
{
// Nur hier liegen die Fehlereinheit (diese eine Zahlung) und der Betriebskontext vor
_logger.LogError(ex, "Zahlung für Bestellung {OrderId} fehlgeschlagen", orderId);
return StatusCode(StatusCodes.Status502BadGateway);
}
}
}
Wird in C# erneut geworfen, gilt als Grundregel, throw; zu verwenden, damit der Stack Trace nicht zerstört wird. Schreibt man stattdessen throw ex;, wird der Stack Trace in dieser Zeile überschrieben und die eigentliche Ursprungsstelle geht verloren.
7.3. Bibliotheksschicht oder gemeinsame Komponente zeigt UI direkt an
Zeigt eine gemeinsame Komponente eine MessageBox an oder entscheidet direkt über den Inhalt einer HTTP-Antwort, brechen sowohl die Wiederverwendbarkeit als auch die Trennung der Verantwortlichkeiten zusammen.
Untere Schichten sind sicherer, wenn sie sich darauf beschränken, einen aussagekräftigen Fehler zurückzugeben.
7.4. OperationCanceledException als Ausfall mit Error loggen
Ein Abbruch ist Teil des Kontrollflusses.
Wird er jedes Mal als Error protokolliert, gehen die echten Ausfälle darin unter.
// NG: Ein breites catch schließt auch den Abbruch ein und loggt ihn als Error
try
{
await _useCase.ImportAsync(file, ct);
}
catch (Exception ex)
{
// Selbst wenn die Benutzerin nur „Abbrechen“ gedrückt hat, landet man hier und Error erscheint
_logger.LogError(ex, "Import fehlgeschlagen");
throw;
}
Da catch-Blöcke von oben nach unten ausgewertet werden, fängt man den Abbruch zuerst mit einem konkreteren Typ ab. Eine when-Klausel hilft dabei, den durch das eigene Token ausgelösten Abbruch nicht mit einer OperationCanceledException aus anderen Gründen — etwa einem internen Timeout — zu verwechseln.
// OK: Der Abbruch wird zuerst abgefangen und vom Fehlerlog getrennt
try
{
await _useCase.ImportAsync(file, ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
// Abbruch durch die Benutzerin oder Shutdown. Teil des Kontrollflusses, daher kein Error
_logger.LogInformation("Import wurde abgebrochen. fileId={FileId}", file.Id);
}
catch (Exception ex)
{
// Hierher gelangen nur unerwartete Fehler. Mit Kontext der Fehlereinheit
// wird einmal das Primärlog geschrieben
_logger.LogError(ex, "Import fehlgeschlagen. fileId={FileId}", file.Id);
throw;
}
7.5. Bei externen Nebenwirkungen unbedacht erneut versuchen
Bei E-Mail-Versand, Zahlungen, Gerätebefehlen oder Dateiverschiebungen führt eine zweite Ausführung derselben Operation oft zu Problemen. Ein Retry gehört nur dorthin, wo sowohl vorübergehender Charakter als auch Idempotenz erkennbar sind.
7.6. Im letzten Handler für unbehandelte Ausnahmen alles reparieren wollen
Dies ist die letzte Absicherung. Kein Ort, den man ins Zentrum des Designs stellt.
Die Wiederherstellungsstrategie ist sicherer eine Schicht vorher angesiedelt — an der Grenze zu Request, Job oder Subsystem.
8. Checkliste für Reviews
Beim Review der Ausnahmebehandlung entstehen in dieser Reihenfolge die wenigsten Lücken.
- Lässt sich in einem Satz sagen, was dieses
catchentscheiden soll? - Kann an dieser Stelle wirklich über Retry / Ergebnisumwandlung / Fortsetzbarkeit / Benutzerantwort entschieden werden?
- Wird derselbe Fehler auch in der oberen Schicht noch mit
Errorprotokolliert, wenn hier geloggt wird? - Werden implementierungsspezifische Ausnahmen der unteren Schicht an der Grenze in aussagekräftige Fehler übersetzt?
- Lässt sich ein halb kaputter Zustand hier zurücksetzen? Falls nicht, wird er als nicht mehr verwendbar behandelt?
- Wird
OperationCanceledExceptionvon gewöhnlichen Fehlern getrennt gehalten? - Ist klar, ob es sich um Fortsetzung je Eintrag, Fehlschlag je Request oder Prozessende handelt?
- Wird vom letzten Handler für unbehandelte Ausnahmen Aufzeichnung erwartet, nicht Wiederherstellung?
- Trägt das Log Kontext der Fehlereinheit wie requestId / userId / batchId / fileId / rowNumber?
- Werden „erwarteter Fehler“ und „gebrochener Grundzustand“ gleich behandelt?
Am wirksamsten ist es in dieser Checkliste, sich jedes Mal in Worten klarzumachen, was dieses catch entscheidet.
Lässt sich das nicht beantworten, ist der catch-Block meist überflüssig oder zu tief angesiedelt.
9. Grobe Übersicht
Zum Schluss eine zusammenfassende Tabelle, die Kapitel 3 und 6 auf ein Blatt komprimiert. Beim Review oder bei der abschließenden Durchsicht fertigen Codes genügt oft dieses eine Blatt.
| Situation | catch |
Log | Fehlerbehandlung |
|---|---|---|---|
| Helper / Utility | Grundsätzlich nicht | Nicht | Nicht |
| Repository / Gateway / SDK-Wrapper | Nur konkrete Ausnahmen abfangen | Normalerweise kein Primärlog | Übersetzung, lokaler Retry, Verbindungen verwerfen |
| UseCase / Application Service | Erwartete Fehler abfangen | Bei Bedarf, falls abgefangen | Ergebnisumwandlung, Teilfehlerbehandlung |
| Grenze UI / Controller / Request / Item / Job | Unerwartete Ausnahmen breit abfangen | Primärlog | Antwort, Meldung, Fortsetzen / Abbruch |
| Handler für unbehandelte Ausnahmen | Nur was durchgesickert ist | Critical |
Abschließende Aufzeichnung, Exit-Pfad |
Im Zweifel genügen diese fünf Punkte.
- In tiefen Schichten nicht breit zugreifen
- An Grenzen abfangen
- Ein Primärlog
- Die abfangende Schicht trägt die Verantwortung
- Die letzte unbehandelte Ausnahme bedeutet Aufzeichnung und Exit-Pfad
10. Zusammenfassung
Ausnahmebehandlung bedeutet nicht: „Wir können überall catchen, also catchen wir überall.“
Die Prüfreihenfolge ist im Wesentlichen diese, und sie genügt.
- Kann an dieser Stelle wirklich entschieden werden?
- Ist hier die Fehlereinheit bekannt?
- Lässt sich der Zustand hier zurücksetzen oder neu aufbauen?
- Führt das Loggen hier zu Duplikaten?
- Ist dies ein Wiederherstellungspunkt oder die letzte Aufzeichnungsstelle?
Wer in dieser Reihenfolge prüft, kann die Aufrufhierarchie deutlich leichter ordnen.
Besonders wichtig sind diese drei Punkte:
- Tiefe Schichten: vor allem Übersetzung und Aufräumen
- Grenzen: vor allem Entscheidungen und das Primärlog
- Der letzte Handler für unbehandelte Ausnahmen: vor allem Aufzeichnung und Exit-Pfad
Anders gesagt gilt als Grundregel: Ausnahmen an Grenzen abfangen, Kontext anfügen und nur dort behandeln, wo eine Wiederherstellung möglich ist.
Ist das einmal festgelegt, werden Code-Reviews und Störungssuche deutlich verlässlicher.
11. Referenzlinks
- .NET: Best Practices für Ausnahmen
- .NET: Ereignis System.AppDomain.UnhandledException
- WPF: Ereignis Application.DispatcherUnhandledException
- Windows Forms: Ereignis Application.ThreadException
- Fehler in ASP.NET Core behandeln
- ASP.NET Core-Middleware
- Windows-Dienste mit BackgroundService
12. Verwandte Artikel
- Entscheidungstabelle: Bei einer unerwarteten Ausnahme beenden oder fortfahren?
- Wenn ein eigener Logger unvermeidlich ist: Welche Mindestanforderungen wirklich nötig sind — praxisnahe Anforderungen und Aspekte für Integrationstests
- Was ist der .NET Generic Host? - Die Grundlage für DI, Konfiguration und Logging
- Wie zieht man die Grenze zwischen Unit-Tests und Integrationstests?
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Entscheidungstabelle: Nach einer unerwarteten Ausnahme beenden oder fortsetzen?
Wenn eine unerwartete Ausnahme auftritt, ordnet dieser Artikel ein, ob die Anwendung beendet oder fortgesetzt werden sollte – aus der Per...
Eine Mindest-Sicherheitscheckliste für die Windows-Anwendungsentwicklung
Ordnet für WPF / WinForms / WinUI / C++ / C#-Businessanwendungen die Grundlagen zu Rechten, Signatur, Updates, Geheimnissen, HTTPS, Einga...
Praktische Best Practices für Multithreading: Java-Edition — Konventionen für das Zeitalter der virtuellen Threads
In Java besteht die bewährte Praxis für Multithreading darin, niemals Threads direkt zu erstellen, sondern auf ExecutorService und virtue...
Praktische Multithreading-Best-Practices: C-Edition — Sicher schreiben nach Win32-API-Konvention
Bei Multithreading in C mit Win32 sind Thread-Erstellung über _beginthreadex, SRW-Locks und Bedingungsvariablen, Interlocked-Funktionen s...
Praktische Multithreading-Best-Practices: C++-Edition — Unfälle mit RAII und jthread strukturell beseitigen
In C++ ist Multithreading eine Welt, in der eine Datenrace zu undefiniertem Verhalten wird. Dieser Artikel behandelt die Falle des std::t...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Technische Beratung und Design-Review
Klärung von Änderungsstrategie, Entwurf und Umgang mit bestehenden Assets.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- In welcher Schicht sollten Ausnahmen abgefangen werden?
- Das Prinzip lautet, in tiefen Schichten nicht breit zu catchen, sondern das Abfangen zu den Grenzen zu verlagern, an denen sich eine Fehlereinheit definieren lässt. Eine einzelne Bildschirmbedienung, eine HTTP-Anfrage, ein Job oder eine Nachricht sind natürliche Abnahmestellen für solche Prozessgrenzen. Maßgeblich ist nicht, wo man catchen kann, sondern wo man verantwortlich über Retry, Ergebnisumwandlung und Fortsetzbarkeit entscheiden kann. Tiefe Helper und Utilities beschränken sich auf Aufräumarbeiten in finally, lokale Rollbacks und die Übersetzung von Ausnahmen.
- Sollte jede Schicht die Ausnahme protokollieren?
- Für einen Fehler sollte grundsätzlich nur ein Primärlog auf Error- oder Critical-Ebene geschrieben werden. Wenn das Repository Error protokolliert, der Service dieselbe Ausnahme erneut mit Error protokolliert und der Controller sie noch einmal protokolliert, stehen bei einem einzigen Vorfall mehrere Kopien desselben Stack Trace nebeneinander, was das Lesen erschwert. Untere Schichten beschränken sich auf Übersetzung und das Anreichern von Kontext, während die obere Grenze, an der Betriebskontext wie requestId und userId zusammenkommt, das Primärlog schreibt. Nur die Schicht, die eine Ausnahme abfängt und in ein Ergebnis umwandelt, trägt die Aufzeichnungsverantwortung für diesen Fehler.
- Wie trennt man erwartete Fehler von unerwarteten Ausnahmen?
- Erwartete Fehler sind Fehler, die im Design bereits vorab entschieden werden können — Validierungsmängel oder NotFound werden auf Ebene des Anwendungsfalls in ein Ergebnis umgewandelt und nicht jedes Mal als Error protokolliert. Auch eine OperationCanceledException durch Benutzerabbruch wird normalerweise nicht als Error behandelt. Ein NullReferenceException dagegen, der einen gebrochenen Grundzustand anzeigt, wird an der Request- oder Job-Grenze mit einem Primärlog versehen und als Fehlerantwort behandelt, während AccessViolationException oder ein schwerwiegender OutOfMemoryException als Critical eingestuft und eher zum Beenden führen. Allein diese Trennung verringert Vorfälle, bei denen wirklich gefährliche Fehler untergehen.
- Was sollte der Handler für unbehandelte Ausnahmen tun?
- AppDomain.UnhandledException, WPFs DispatcherUnhandledException oder WinForms' ThreadException sind kein Wiederherstellungspunkt, sondern die letzte Aufzeichnungsstelle. Die Hauptaufgaben sind das abschließende Log, das Flushen, ein Pfad zur Dump-Erfassung sowie das Einrichten von Exitcode und Neustartpfad. Da an dieser Stelle der Zustand bereits beschädigt sein kann, bedeutet die scheinbare Fortsetzbarkeit nicht, dass ein Fortsetzen auch richtig ist. Die Wiederherstellungsstrategie gehört sicherer an die davorliegende Request- oder Job-Grenze.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.