Fehlerbehandlung beginnt dort, wo der Happy Path endet
Software lässt sich erstaunlich leicht demonstrieren.
Man startet das Programm.
Die Verbindung funktioniert.
Die API antwortet.
Alle Daten sind vorhanden.
Jede Operation ist erfolgreich.
Am Ende erscheint:
SUCCESS
Damit ist bewiesen, dass der Happy Path funktioniert.
Mehr aber auch nicht.
Produktionsreife beginnt bei einer anderen Frage:
Was passiert, wenn die Operation nur teilweise funktioniert?
Genau dort wird Fehlerbehandlung interessant.
Nicht beim catch-Block.
Sondern bei der Frage, welchen Zustand das System nach einem Fehler tatsächlich besitzt.
Der Happy Path ist nur ein möglicher Ablauf
Nehmen wir ein einfaches Tool, das Daten über eine API verändert.
Der ideale Ablauf sieht so aus:
Connect
↓
Discover
↓
Process
↓
Verify
↓
Completed
Das lässt sich einfach implementieren.
Die Realität sieht eher so aus:
Connect
├── Authentication failed
├── Permission denied
└── Success
↓
Discover
├── Timeout
├── Pagination failure
└── Success
↓
Process
├── Success
├── Partial success
├── Rate limited
├── Server error
└── Connection lost
↓
Verify
├── Expected state reached
└── Residual state remains
Das ist nicht die Ausnahme.
Das ist der normale Zustandsraum eines verteilten Systems.
try/catch ist noch keine Fehlerstrategie
Eine typische erste Fehlerbehandlung sieht so aus:
try {
Invoke-Something
}
catch {
Write-Error $_
}
Das ist technisch sinnvoll.
Aber es beantwortet nur:
Ist eine Exception aufgetreten?
Es beantwortet nicht:
- Was wurde bereits erfolgreich ausgeführt?
- Was wurde nicht ausgeführt?
- Was darf wiederholt werden?
- Was darf nicht wiederholt werden?
- Kann die Operation fortgesetzt werden?
- Ist der aktuelle Zustand bekannt?
- Muss der Benutzer eingreifen?
- Ist das Ergebnis verifiziert?
Das sind die eigentlich interessanten Fragen.
Fehler sind nicht alle gleich
Ein wichtiger Schritt ist die Klassifikation von Fehlern.
Angenommen, eine API antwortet mit:
429 Too Many Requests
Das bedeutet im Wesentlichen:
Nicht jetzt.
Ein:
500 Internal Server Error
bedeutet eher:
Der Server konnte die Anfrage momentan nicht korrekt verarbeiten.
Ein:
403 Forbidden
bedeutet dagegen:
Diese Operation ist nicht erlaubt.
Und ein:
400 Bad Request
häufig:
Mit dieser Anfrage stimmt grundsätzlich etwas nicht.
Diese Fehler sollten nicht identisch behandelt werden.
Retryable und Non-Retryable Errors
Eine erste sinnvolle Klassifikation lautet:
Retryable
Non-Retryable
Typische Kandidaten für Retry:
429
500
502
503
504
Timeout
temporärer Netzwerkfehler
Typische Kandidaten gegen blindes Retry:
400
401
403
404
Natürlich hängt die genaue Semantik von der jeweiligen API ab.
Der wichtige Punkt ist:
Retry ist eine fachliche Entscheidung, nicht einfach eine Reaktion auf jede Exception.
Blindes Retry kann Probleme verschlimmern
Dieser Code wirkt robust:
try {
Invoke-Operation
}
catch {
Start-Sleep -Seconds 2
Invoke-Operation
}
Aber was passiert bei:
403 Forbidden
?
Man wartet zwei Sekunden und stellt dieselbe verbotene Anfrage erneut.
Oder bei:
400 Bad Request
?
Die Anfrage wird durch Warten nicht richtiger.
Retry ohne Klassifikation erzeugt nur zusätzliche Last und verzögert die eigentliche Fehlermeldung.
Retry braucht Grenzen
Auch bei retrybaren Fehlern darf die Anwendung nicht endlos weitermachen.
Ein typisches Modell:
Attempt 1
↓ failed
wait 1s
Attempt 2
↓ failed
wait 2s
Attempt 3
↓ failed
wait 4s
FAILED
Das ist Exponential Backoff.
Vereinfacht:
1
2
4
8
16
...
Dadurch hämmert der Client bei einer Störung nicht permanent auf den Server.
Retry-After ist besser als Raten
Bei HTTP 429 liefern APIs häufig einen Hinweis:
Retry-After: 10
Dann sagt der Server explizit:
Versuche es in zehn Sekunden erneut.
In diesem Fall sollte der Client diese Information respektieren.
Also nicht:
Start-Sleep -Seconds 2
sondern sinngemäß:
Start-Sleep -Seconds $retryAfter
Das ist ein kleines Detail mit großer Wirkung.
Eine robuste Anwendung arbeitet mit dem Server zusammen, statt gegen ihn.
Jitter verhindert synchronisierte Retries
Bei vielen Clients entsteht noch ein anderes Problem.
Angenommen, ein Dienst fällt kurz aus.
1’000 Clients erhalten gleichzeitig einen Fehler.
Alle warten exakt fünf Sekunden.
Dann senden alle 1’000 Clients gleichzeitig erneut.
Der Dienst bekommt die nächste Lastspitze.
Deshalb ergänzt man Backoff häufig um zufällige Abweichung:
Backoff
+
Jitter
Aus:
5 Sekunden
wird beispielsweise:
4.7 Sekunden
5.3 Sekunden
5.8 Sekunden
...
Für ein einzelnes kleines PowerShell-Tool ist das vielleicht nicht entscheidend.
Das Konzept ist trotzdem wichtig, wenn man robuste verteilte Systeme verstehen will.
Ein erfolgreicher HTTP-Request bedeutet nicht automatisch Erfolg
Nehmen wir an:
HTTP 200 OK
Dann wissen wir zunächst nur:
Die Anfrage wurde auf Protokollebene erfolgreich beantwortet.
Bei Batch-APIs wird es noch deutlicher.
Ein Batch-Request kann selbst erfolgreich sein:
HTTP 200
während einzelne Operationen darin fehlschlagen.
Zum Beispiel:
Batch
────────────────
Request 1 200
Request 2 200
Request 3 429
Request 4 403
Request 5 200
Der Batch war transporttechnisch erfolgreich.
Die fachliche Operation war es nur teilweise.
Partial Failure ist der Normalfall, den viele vergessen
Stellen wir uns 20 Änderungen vor.
Das Ergebnis:
18 erfolgreich
1 temporär fehlgeschlagen
1 dauerhaft fehlgeschlagen
Eine zu einfache Funktion liefert vielleicht:
return $false
Das ist zu wenig Information.
Denn:
18 Operationen sind bereits erledigt.
Ein komplettes Retry aller 20 Elemente könnte nun problematisch sein.
Deshalb sollte das Ergebnis präziser sein.
Zum Beispiel:
[pscustomobject]@{
SucceededIds = @(...)
RetryIds = @(...)
FailedIds = @(...)
}
Jetzt weiß die nächste Schicht genau, was passiert ist.
Counts reichen irgendwann nicht mehr
Für eine Anzeige ist das hier praktisch:
[pscustomobject]@{
Succeeded = 18
Retrying = 1
Failed = 1
}
Für Resume reicht es nicht.
Denn nach einem Neustart muss die Anwendung wissen:
Welche 18 Elemente waren erfolgreich?
Deshalb braucht man bei zustandsbehafteter Verarbeitung Identitäten:
SucceededIds
FailedIds
PendingIds
Nicht nur:
18
1
1
Das ist ein gutes Beispiel dafür, wie eine neue Anforderung das Datenmodell verändert.
Fehlende Antworten sind ebenfalls ein Zustand
Ein subtiler Fall entsteht bei Batch-Verarbeitung.
Man sendet:
20 Requests
und erhält:
19 Responses
Was ist mit Request Nummer 20?
Er ist nicht:
successful
Aber auch nicht nachweislich:
failed
Sein Zustand ist:
unknown
Das ist wichtig.
Eine robuste Implementierung sollte deshalb vergleichen:
Sent Request IDs
-
Received Response IDs
=
Missing Response IDs
Diese fehlenden IDs müssen bewusst behandelt werden.
Zum Beispiel erneut versucht oder als unbestimmt protokolliert werden.
Sie dürfen nicht einfach verschwinden.
„Unknown“ ist ein legitimer Zustand
Entwickler modellieren gerne:
Success
Failure
Verteilte Systeme besitzen aber häufig einen dritten Zustand:
Unknown
Beispiel:
Der Client sendet eine Anfrage.
Dann bricht die Netzwerkverbindung ab.
Hat der Server die Operation ausgeführt?
Vielleicht.
Vielleicht nicht.
Der Client weiß es nicht.
Das ist fundamental.
Genau hier wird Idempotenz wichtig
Angenommen, die Operation lautet:
Set isRead = true
Dann ist eine Wiederholung normalerweise unproblematisch:
false → true
und erneut:
true → true
Der Endzustand bleibt gleich.
Das ist eine idempotente Operation.
Vereinfacht:
Dieselbe Operation mehrfach auszuführen erzeugt denselben Endzustand wie einmalige Ausführung.
Nicht jede Operation ist idempotent
Betrachten wir dagegen:
Create invoice
Wenn die Antwort verloren geht und wir die Operation erneut ausführen, könnten zwei Rechnungen entstehen.
Oder:
Charge credit card
Ein blindes Retry wäre offensichtlich gefährlich.
Deshalb muss Retry immer zusammen mit der Frage betrachtet werden:
Ist diese Operation sicher wiederholbar?
Idempotency Keys
Viele APIs lösen dieses Problem mit einem Idempotency Key.
Beispielsweise:
Idempotency-Key:
550e8400-e29b-41d4-a716-446655440000
Wird dieselbe Operation mit demselben Key erneut gesendet, kann der Server erkennen:
Diese Operation habe ich bereits verarbeitet.
Das verhindert doppelte Seiteneffekte.
Nicht jede API unterstützt das.
Aber das zugrunde liegende Konzept ist für zuverlässige Integrationen zentral.
Retry und Idempotenz gehören zusammen
Die Regel lautet deshalb nicht:
Bei Netzwerkfehlern einfach erneut versuchen.
Sondern:
Fehler
↓
Retryable?
↓
Operation idempotent?
↓
Zustand bekannt?
↓
Retry sicher?
Erst dann sollte die Wiederholung stattfinden.
Resume ist Retry über Prozessgrenzen hinweg
Retry passiert normalerweise innerhalb desselben Programmlaufs.
Resume erweitert dieselbe Idee.
Retry
→ Wiederholung innerhalb des Runs
Resume
→ Wiederaufnahme nach Ende des Runs
Dafür muss Zustand persistiert werden.
Zum Beispiel in einem Checkpoint.
Was ist ein Checkpoint?
Ein Checkpoint speichert genügend Informationen, um nach einem Abbruch sinnvoll weiterarbeiten zu können.
Beispielsweise:
{
"runId": "12345",
"startedAt": "2026-09-05T14:00:00Z",
"succeededIds": [
"A",
"B",
"C"
]
}
Beim nächsten Start kann die Anwendung erkennen:
A → bereits erledigt
B → bereits erledigt
C → bereits erledigt
D → noch offen
Sie muss nicht wieder bei null beginnen.
Ein Checkpoint ist keine Logdatei
Das ist eine wichtige Unterscheidung.
Ein Log beantwortet:
Was ist passiert?
Ein Checkpoint beantwortet:
Was muss ich wissen, um korrekt weiterzumachen?
Das sind unterschiedliche Anforderungen.
Ein Log kann beispielsweise enthalten:
14:32:01 Processing A
14:32:02 Processing B
14:32:03 Retry B
14:32:05 Processing C
Ein Checkpoint braucht vielleicht nur:
{
"succeededIds": ["A", "B", "C"]
}
Die Datenstrukturen sollten deshalb nicht verwechselt werden.
„Processed“ ist ein gefährliches Wort
Nehmen wir an, der Checkpoint speichert:
{
"processedIds": [
"A",
"B"
]
}
Was bedeutet processed?
Für A:
Operation erfolgreich.
Für B:
Operation versucht, aber fehlgeschlagen.
Wenn beide als „processed“ gespeichert werden, entsteht beim Resume ein schwerer Fehler.
B wird übersprungen.
Obwohl die gewünschte Änderung nie erfolgreich war.
Präzise Sprache verhindert Fehler
Besser:
AttemptedIds
SucceededIds
FailedIds
Diese Begriffe haben klare Semantik.
Für Resume ist meistens entscheidend:
SucceededIds
Denn nur diese Elemente dürfen sicher als erledigt betrachtet werden.
Das ist kein Naming-Detail.
Das ist Teil der Korrektheit.
Checkpoints brauchen eine Commit-Semantik
Man kann das gedanklich ähnlich wie eine kleine Transaktion betrachten.
Operation versuchen
↓
Erfolg bestätigt?
↓
Ja
↓
Checkpoint aktualisieren
Nicht:
Operation versuchen
↓
Checkpoint aktualisieren
↓
später herausfinden, ob es funktioniert hat
Der Checkpoint sollte den bestätigten Zustand repräsentieren.
Wann sollte ein Checkpoint geschrieben werden?
Hier entsteht ein Trade-off.
Angenommen, ein Folder enthält 10’000 Elemente.
Variante A:
10'000 verarbeiten
↓
Checkpoint schreiben
Wenn bei Element 9’999 der Prozess abstürzt, fehlen viele erfolgreiche Operationen im Checkpoint.
Variante B:
jedes Element
↓
Checkpoint schreiben
Das ist sicherer, erzeugt aber sehr viele Schreiboperationen.
Eine pragmatische Lösung:
Batch verarbeiten
↓
erfolgreiche IDs übernehmen
↓
Checkpoint schreiben
Damit entspricht die Persistenz der natürlichen Arbeitseinheit.
Checkpoint-Schreiben selbst kann scheitern
Auch das wird gerne vergessen.
Die Verarbeitung funktioniert.
Dann kann der Checkpoint nicht gespeichert werden:
Disk full
Permission denied
File locked
Was nun?
Die externe Operation ist bereits passiert.
Der lokale Zustand wurde aber nicht persistiert.
Genau deshalb ist Idempotenz wieder wichtig.
Beim Resume könnten bereits erfolgreiche Operationen erneut entdeckt werden.
Wenn deren Wiederholung sicher ist, bleibt das System beherrschbar.
Checkpoints sollten atomar geschrieben werden
Ein weiteres Problem:
Das Programm schreibt eine JSON-Datei.
Mitten während des Schreibens stürzt der Prozess ab.
Zurück bleibt:
{
"succeededIds": [
"A",
"B",
Der Checkpoint ist kaputt.
Eine typische Strategie:
checkpoint.tmp schreiben
↓
vollständig erfolgreich?
↓
rename / replace
↓
checkpoint.json
Das reduziert das Risiko eines teilweise geschriebenen Zustands.
Ein Checkpoint braucht möglicherweise eine Version
Heute sieht der Checkpoint so aus:
{
"succeededIds": []
}
In sechs Monaten vielleicht:
{
"version": 2,
"runId": "...",
"scope": "...",
"succeededIds": []
}
Ohne Versionierung kann ein neues Programm einen alten Checkpoint falsch interpretieren.
Deshalb kann selbst bei kleinen Tools sinnvoll sein:
{
"schemaVersion": 1
}
Das kostet fast nichts und schafft eine klare Evolutionsgrenze.
Resume muss den Kontext validieren
Ein gespeicherter Checkpoint darf nicht blind verwendet werden.
Angenommen, der Checkpoint stammt von:
Mailbox A
und der Benutzer startet:
Mailbox B
Wenn nur Message IDs gespeichert sind, könnte das gefährlich werden.
Deshalb sollte ein Checkpoint seinen Kontext kennen:
Target
User
Operation
Run
Schema Version
Beim Resume kann geprüft werden:
Passt dieser Checkpoint überhaupt zu diesem Lauf?
Ein Checkpoint ist kein Ersatz für Idempotenz
Es wäre verführerisch zu sagen:
Wir speichern alle erfolgreichen IDs, also brauchen wir keine idempotenten Operationen.
Das stimmt nicht.
Zwischen:
Operation erfolgreich
und:
Checkpoint erfolgreich gespeichert
existiert immer ein kleines Zeitfenster.
Der Prozess kann genau dort abstürzen.
Dann wurde die Operation durchgeführt, aber nicht persistiert.
Beim nächsten Start wird sie möglicherweise erneut ausgeführt.
Deshalb ist die Kombination stark:
Idempotente Operation
+
Checkpoint
Nicht entweder/oder.
Verification ist eine eigene Phase
Nach erfolgreicher Verarbeitung kommt eine weitere Frage:
Ist der gewünschte Endzustand tatsächlich erreicht?
Das sollte nicht automatisch aus den Operationen abgeleitet werden.
Angenommen:
56 Änderungen angefordert
56 erfolgreiche Responses
Das ist gut.
Aber der eigentliche Auftrag könnte lauten:
Es sollen keine ungelesenen Nachrichten mehr vorhanden sein.
Dann ist die relevante Prüfung:
Remaining unread = ?
Operationserfolg und Zustandserfolg
Diese Unterscheidung ist fundamental.
Operation Success
bedeutet:
Die angeforderten Operationen wurden erfolgreich ausgeführt.
State Success
bedeutet:
Das System befindet sich jetzt im gewünschten Zustand.
Beides kann auseinanderfallen.
Beispiel
Wir entdecken:
56 ungelesene Nachrichten
Wir ändern alle 56.
Die API meldet:
56 erfolgreich
Währenddessen kommt eine neue Nachricht an.
Post-Verification:
1 ungelesene Nachricht
War die Verarbeitung erfolgreich?
Ja.
Ist der gewünschte globale Zustand:
0 unread
erreicht?
Nein.
Beide Aussagen können gleichzeitig wahr sein.
Verification braucht eine definierte Wahrheit
Auch hier ist wichtig, was verifiziert wird.
Angenommen, eine API liefert Folder-Metadaten:
unreadItemCount = 10
Eine konkrete Query:
isRead = false
findet aber nur:
8
Wenn wir bereits wissen, dass Metadaten und tatsächliche Query voneinander abweichen können, sollten wir bei der Post-Verification nicht plötzlich die Metadaten zur endgültigen Wahrheit erklären.
Die Verifikation sollte möglichst dieselbe fachliche Definition verwenden wie die ursprüngliche Auswahl.
Also:
Pre-flight:
isRead = false
Post-flight:
isRead = false
Das macht die Semantik konsistent.
Verification ist nicht dasselbe wie Retry
Wenn nach der Verarbeitung noch zwei Elemente übrig sind, gibt es verschiedene Möglichkeiten.
Vielleicht:
zwei neue Elemente sind entstanden
Vielleicht:
zwei ursprüngliche Operationen sind fehlgeschlagen
Vielleicht:
eventual consistency
Vielleicht:
falsche Query
Deshalb sollte Verification zunächst feststellen.
Nicht automatisch reparieren.
Observe
↓
Classify
↓
Decide
Nicht:
Observe
↓
blind retry everything
Eventual Consistency
Verteilte Systeme können Änderungen verzögert sichtbar machen.
Eine Mutation ist erfolgreich.
Direkt danach liefert eine andere Query noch den alten Zustand.
Kurz später stimmt alles.
Das nennt man Eventual Consistency.
Deshalb kann eine Verification-Strategie beispielsweise lauten:
Verify
↓
Residual state?
↓
kurz warten
↓
Verify again
↓
final result
Auch hier sollten Retries begrenzt sein.
Erfolg braucht mehrere Ebenen
Für ein robustes Tool kann ein Ergebnis deshalb differenzierter aussehen:
[pscustomobject]@{
Attempted = 56
Succeeded = 55
Failed = 1
Verified = $false
Remaining = 1
}
Das ist viel aussagekräftiger als:
$success = $false
SUCCESS ist eine Behauptung
Das ist besonders für TUIs relevant.
Ein grüner Abschluss-Screen mit:
SUCCESS
ist keine Dekoration.
Er ist eine fachliche Behauptung.
Die Anwendung sollte deshalb definieren, wann dieser Zustand erreicht ist.
Zum Beispiel:
Completed
+
Failed = 0
+
Verification completed
+
Remaining = 0
Erst dann:
VERIFIED SUCCESS
Es gibt mehr als Success und Failure
Ein ausgereiftes Tool kann Ergebnisse besitzen wie:
Completed
CompletedWithFailures
VerificationFailed
Cancelled
Interrupted
ResumeAvailable
Failed
Nicht jede Operation passt sinnvoll in:
true / false
Gerade Batch-Systeme profitieren von präziseren Endzuständen.
Fehler sollten den nächsten Schritt erklären
Eine Fehlermeldung wie:
HTTP 503
ist technisch korrekt.
Für den Benutzer ist hilfreicher:
PROCESSING INTERRUPTED
117 / 184 items completed.
The service is temporarily unavailable.
Checkpoint saved.
The operation can be resumed.
Das beantwortet:
Was ist passiert?
Was wurde bereits erledigt?
Ist der Zustand sicher?
Was kann ich jetzt tun?
Das ist gute Fehlerbehandlung.
Fehlerbehandlung ist Teil der UX
Das wird oft unterschätzt.
UX wird gerne mit:
- Farben
- Buttons
- Layout
- Animation
verbunden.
Bei technischen Werkzeugen entsteht gute UX aber besonders in Fehlerfällen.
Ein gutes Tool sagt nicht nur:
Something went wrong.
Es sagt:
was
wo
wie weit
welcher Zustand
ob Retry sinnvoll ist
ob Resume möglich ist
Das schafft Vertrauen.
Logging und Benutzeroberfläche haben unterschiedliche Aufgaben
Ein Log darf technisch sein:
2026-09-05T14:32:01Z
PATCH /messages/abc
HTTP 503
attempt=3
Die TUI sollte eher sagen:
SERVICE TEMPORARILY UNAVAILABLE
Retry 3 / 5
Continuing in 8 seconds
Beides beschreibt dasselbe Ereignis.
Aber für unterschiedliche Zielgruppen.
Quiet Mode braucht ebenfalls saubere Fehler
Maschinenlesbare Ausgabe sollte Fehler strukturiert liefern.
Nicht:
ERROR!!!
Something failed.
sondern beispielsweise:
{
"status": "partial_failure",
"processed": 117,
"failed": 1,
"remaining": 67,
"resumeAvailable": true
}
Damit kann ein anderes Programm reagieren.
Fehlerbehandlung wird dadurch Teil des öffentlichen Schnittstellenvertrags.
Exit Codes gehören dazu
Für CLI-Tools sind Exit Codes ebenfalls wichtig.
Zum Beispiel:
0
→ erfolgreich
1
→ allgemeiner Fehler
2
→ teilweise Verarbeitung
3
→ Verifikation fehlgeschlagen
Die konkrete Konvention ist projektspezifisch.
Entscheidend ist:
Ein automatisierter Aufrufer sollte nicht den Bildschirmtext analysieren müssen, um herauszufinden, ob etwas funktioniert hat.
Fehlerbehandlung beginnt beim Design
Die wichtigste Erkenntnis ist deshalb:
Robustheit kann nicht am Ende mit ein paar catch-Blöcken hinzugefügt werden.
Bereits das Datenmodell muss Fehlerfälle ausdrücken können.
Wenn eine Funktion nur:
$true
oder:
$false
zurückgeben kann, kann sie keinen Partial Failure beschreiben.
Wenn ein Checkpoint nur:
ProcessedIds
kennt, kann er Success und Failure nicht unterscheiden.
Wenn die TUI nur:
Processing
Completed
kennt, kann sie Retry und Resume nicht sinnvoll darstellen.
Fehlerbehandlung beeinflusst deshalb:
Data Model
State Model
API Layer
Persistence
TUI
Machine Output
Tests
Sie ist Architektur.
Fehlerfälle sollten zuerst modelliert werden
Bei kritischer Logik lohnt sich eine kleine Übung.
Vor der Implementierung eine Tabelle erstellen:
| Situation | Retry? | Zustand bekannt? | Resume? |
|---|---|---|---|
| 200 | nein | ja | – |
| 429 | ja | ja | ja |
| 503 | ja | meist | ja |
| 403 | nein | ja | nach Korrektur |
| Timeout nach Request | eventuell | nein | abhängig von Idempotenz |
| Partial Batch | teilweise | ja | ja |
| Prozessabbruch | nein | teilweise | ja |
Plötzlich werden Architekturfragen sichtbar, bevor sie Bugs werden.
Fehlerpfade brauchen Tests
Der Happy Path sollte natürlich getestet werden.
Aber bei einem zuverlässigen Tool sind diese Tests mindestens genauso interessant:
429
→ Retry
→ Success
500
→ Retry
→ Retry
→ Exhausted
Batch:
19 Success
1 Failure
20 Requests
19 Responses
Operation erfolgreich
Checkpoint-Schreiben schlägt fehl
Resume
→ Failed ID wird erneut verarbeitet
Verification
→ Remaining > 0
Das sind die Fälle, die Architekturfehler sichtbar machen.
Chaos im Kleinen
Man braucht dafür kein großes Chaos-Engineering-System.
Schon gezielte simulierte Fehler sind wertvoll.
Beispielsweise:
Was passiert, wenn Request Nummer 7 fehlschlägt?
Oder:
Was passiert, wenn nach Batch 3 der Prozess beendet wird?
Oder:
Was passiert, wenn die Verification andere Werte findet?
Diese Fragen testen nicht nur Code.
Sie testen das Modell des Systems.
Der Happy Path versteckt schlechte Architektur
Das ist vielleicht die wichtigste Beobachtung.
Viele strukturelle Fehler sind im Happy Path unsichtbar.
Wenn alles funktioniert:
Request
↓
Success
↓
Request
↓
Success
↓
Done
braucht man weder:
- Retry-State
- Failed IDs
- Checkpoints
- Resume
- Verification
- Unknown State
Erst Fehlerfälle zwingen das System, seine tatsächliche Semantik offenzulegen.
Deshalb sind sie so wertvoll.
Reliability ist kein einzelnes Feature
Man kann nicht einfach sagen:
Feature:
Robust Error Handling ✓
Reliability entsteht aus mehreren Eigenschaften:
klare Fehlerklassifikation
+
begrenztes Retry
+
Backoff
+
Idempotenz
+
Partial-Failure-Modell
+
Checkpoints
+
Resume
+
Verification
+
präzise Resultate
+
Tests
Keine davon allein macht ein System zuverlässig.
Zusammen ergeben sie ein belastbares Verhalten.
Ein robustes Verarbeitungsmodell
Für viele Batch-Tools lässt sich der Ablauf ungefähr so zusammenfassen:
DISCOVER
↓
BUILD WORK QUEUE
↓
PROCESS BATCH
↓
CLASSIFY RESULTS
├── Succeeded
├── Retryable
├── Failed
└── Unknown
↓
RETRY SAFE OPERATIONS
↓
COMMIT CHECKPOINT
↓
NEXT BATCH
↓
VERIFY FINAL STATE
↓
REPORT RESULT
Das ist deutlich mehr als:
foreach item
try
process
catch
retry
Aber genau diese zusätzlichen Schritte machen den Unterschied.
Das Interessante daran: Es gilt weit über PowerShell hinaus
Diese Konzepte sind nicht PowerShell-spezifisch.
Sie gelten für:
- REST-Clients
- Background Jobs
- Message Processing
- Datenmigrationen
- Synchronisationsdienste
- Importer
- Deployment Tools
- Batch-Verarbeitung
- Cloud-Automation
Die konkrete Syntax ändert sich.
Die Fragen bleiben dieselben:
Was wurde versucht?
Was war erfolgreich?
Was ist fehlgeschlagen?
Was ist unbekannt?
Was darf wiederholt werden?
Was wurde persistiert?
Wie kann weitergemacht werden?
Wie wissen wir, dass wir fertig sind?
Wer diese Fragen beantworten kann, hat einen großen Teil des Reliability-Problems bereits verstanden.
Was ich bei einem neuen Tool heute zuerst definieren würde
Noch bevor die aufwendige Fehlerbehandlung implementiert wird:
1. Was ist die atomare Arbeitseinheit?
eine Nachricht
eine Datei
ein Datensatz
ein API-Request
2. Was bedeutet Erfolg?
Request angenommen?
Mutation bestätigt?
Endzustand verifiziert?
3. Ist die Operation idempotent?
4. Welche Fehler sind retrybar?
5. Welche Zustände können nach einem Abbruch existieren?
6. Welche Informationen braucht Resume?
7. Was muss ein Checkpoint speichern?
8. Wie wird der Endzustand verifiziert?
Wenn diese Fragen klar sind, wird die Implementierung deutlich einfacher.
Fazit
Der Happy Path beweist, dass ein System unter idealen Bedingungen funktioniert.
Produktionsreife zeigt sich unter nicht idealen Bedingungen.
Nicht die Frage:
Funktioniert die API?
ist dann entscheidend.
Sondern:
Wissen wir nach einem Fehler noch, was tatsächlich passiert ist?
Ein robustes System unterscheidet deshalb zwischen:
Attempted
Succeeded
Failed
Retryable
Unknown
Verified
Es wiederholt Operationen nicht blind.
Es versteht Idempotenz.
Es persistiert bestätigten Fortschritt.
Es kann nach einem Abbruch weiterarbeiten.
Und es prüft am Ende nicht nur, ob Requests erfolgreich waren, sondern ob der gewünschte Zustand tatsächlich erreicht wurde.
Die wichtigste Erkenntnis dieser Serie lautet deshalb:
Fehlerbehandlung bedeutet nicht, Exceptions abzufangen. Sie bedeutet, auch nach einem teilweise gescheiterten Ablauf noch eindeutig zu wissen, in welchem Zustand sich das System befindet und wie es sicher weitergehen kann.
Genau dort endet der Happy Path.
Und dort beginnt Reliability.