05.09.2026 · Fabricio Ruch

Fehlerbehandlung beginnt dort, wo der Happy Path endet

AutomationProgramming PrinciplesSoftware EngineeringSystem Engineering

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:

SituationRetry?Zustand bekannt?Resume?
200neinja
429jajaja
503jameistja
403neinjanach Korrektur
Timeout nach Requesteventuellneinabhängig von Idempotenz
Partial Batchteilweisejaja
Prozessabbruchneinteilweiseja

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.