PowerShell-Fehlerbehandlung und Retry-Design — Von der try/catch-Falle bis zu Exit Codes und bewährten Retry-Verfahren

· · PowerShell, Windows, Fehlerbehandlung, Retry, Automatisierung, Betriebsoptimierung, Skript, Aufgabenplanung

„Der nächtliche Batch-Job war fehlgeschlagen, aber die Aufgabenplanung zeigte trotzdem Erfolg (0x0) an, und niemand hat es bemerkt“ — „Ich habe ein try/catch geschrieben, aber die Ausführung erreicht den catch-Block nie“ — „Es fällt nur einmal im Monat wegen einer kurzen Netzwerkunterbrechung aus.“ Sobald Sie ein PowerShell-Skript in den produktiven Betrieb bringen, tauchen solche Klagen garantiert auf. Zwischen einem Skript, das Sie zufriedenstellt, wenn Sie es von Hand ausführen, und einem, das jede Nacht unbeaufsichtigt läuft, steht eine Mauer: das Design von Fehlerbehandlung und Retries.

Das Tückische daran ist, dass sich das Fehlermodell von PowerShell subtil von dem Ausnahmemodell unterscheidet, das Sie aus den meisten Programmiersprachen kennen. „Ein Fehler wurde ausgelöst, aber die Verarbeitung lief einfach weiter“ und „Ich dachte, ich hätte ihn abgefangen, aber er ist mir durchgerutscht“ sind in den meisten Fällen keine Bugs — PowerShell verhält sich hier genau spezifikationsgemäß. Wer Code schreibt, ohne den Mechanismus zu verstehen, produziert am laufenden Band Skripte, die Fehlschläge verschlucken und beenden, als wäre alles erfolgreich gewesen.

Dieser Artikel richtet sich an interne IT-Mitarbeiter und Entwickler, die mit PowerShell routinemäßige interne Arbeit automatisieren. Gestützt auf die offizielle Dokumentation geht er auf die Unterscheidung zwischen abschließenden und nicht abschließenden Fehlern ein, auf die Beurteilung, ob ein nativer Befehl erfolgreich war, auf ein Exit-Code-Design, mit dem Aufgabenplanung und Monitoring-Werkzeuge Erfolg von Fehlschlag unterscheiden können, sowie auf ein Retry-Muster, das vorübergehende Fehler übersteht.

1. Das Wichtigste zuerst

  • PowerShell-Fehler gliedern sich in „nicht abschließende Fehler“ und „abschließende Fehler“ (anweisungsabschließend oder skriptabschließend). Ein nicht abschließender Fehler zeigt eine Meldung an und lässt die Pipeline weiterlaufen; standardmäßig erreicht er try/catch nie.1
  • Der Standardweg zur Abhilfe ist, jedem Befehl, den try/catch abfangen soll, -ErrorAction Stop hinzuzufügen. Stop stuft einen nicht abschließenden Fehler zu einem abschließenden hoch, damit catch ihn behandeln kann. Alternativ können Sie $ErrorActionPreference (Standard: Continue) am Anfang des Skripts auf Stop setzen.12
  • -ErrorAction überschreibt $ErrorActionPreference für genau diesen einen Befehl. Beide sind jedoch nicht vollkommen symmetrisch — -ErrorAction steuert nur nicht abschließende Fehler.1
  • Ein Fehlschlag eines nativen Befehls (robocopy, git, eine externe EXE) wird standardmäßig nicht zu einem PowerShell-Fehler. Ein von Null verschiedener Exit Code setzt $? auf $false und landet in $LASTEXITCODE, aber es wird kein ErrorRecord erzeugt, und catch wird nie erreicht. Beurteilen Sie Erfolg oder Fehlschlag anhand von $LASTEXITCODE.1
  • In PowerShell 7.4 wurde $PSNativeCommandUseErrorActionPreference zu einem regulären Feature. Setzen Sie es auf $true, löst ein von Null verschiedener Exit Code einen nicht abschließenden Fehler aus; kombiniert mit $ErrorActionPreference = 'Stop' kann try/catch ihn abfangen (Standard ist $false).32
  • In catch ist $_ das ErrorRecord. $_.Exception liefert die Ausnahme selbst, und bei einem hochgestuften Fehler führt $_.Exception.ErrorRecord zurück zu den ursprünglichen Fehlerinformationen. Ein catch-Block mit angegebenem Ausnahmetyp erlaubt es, nur die erwarteten Fehler gezielt einzeln zu behandeln.14
  • Melden Sie Erfolg oder Fehlschlag stets über den Exit Code nach außen. Setzen Sie den Exit Code mit dem Schlüsselwort exit; wird das Skript mit pwsh -File / powershell.exe -File gestartet, wird dieser Wert zum Exit Code des Prozesses. Ohne exit ist ein normaler Abschluss 0 und eine unbehandelte Ausnahme 1.56
  • Retries folgen drei Prinzipien: auf vorübergehende Fehler beschränkt, mit Obergrenze versehen und idempotent. Übertünchen Sie fachliche Fehler nicht mit Retries; verlängern Sie das Intervall per exponentiellem Backoff; und gestalten Sie die Verarbeitung so, dass ein erneuter Lauf niemals eine doppelte Verarbeitung verursacht. Erst wenn alle drei Punkte erfüllt sind, haben Sie ein Skript, das sich tatsächlich gefahrlos erneut ausführen lässt.

2. Zwei Fehlerarten — Warum try/catch nicht alles abfängt

PowerShell-Fehler gliedern sich in drei Kategorien: nicht abschließende Fehler (melden, ohne die Pipeline zu stoppen), anweisungsabschließende Fehler (nur diese Anweisung stoppen und mit der nächsten fortfahren) und skriptabschließende Fehler (den gesamten Aufrufstapel zurückwickeln).1

In der Praxis liegt die Falle beim nicht abschließenden Fehler. Wenn ein Cmdlet wie Get-Content oder Get-ChildItem die Verarbeitung eines bestimmten Eingabewerts nicht durchführen kann, löst es dabei typischerweise einen nicht abschließenden Fehler aus — eine rote Fehlermeldung erscheint, die Verarbeitung läuft aber weiter und erreicht weder try/catch noch trap.1

# [Falle] catch wird nie ausgeführt, und "Done" wird trotzdem ausgegeben
try {
    Get-Content -Path 'C:\Data\does-not-exist.txt'   # nicht abschließender Fehler
    Write-Host 'Done'                                 # wird ausgeführt, obwohl die Zeile darüber fehlgeschlagen ist
}
catch {
    Write-Host 'This never runs'
}

# [Standardlösung] -ErrorAction Stop stuft ihn zu einem abschließenden Fehler hoch, damit catch ihn behandeln kann
try {
    Get-Content -Path 'C:\Data\does-not-exist.txt' -ErrorAction Stop
    Write-Host 'Done'                                 # wird übersprungen, wenn die Zeile darüber einen Fehler auslöst
}
catch {
    Write-Host "Caught: $($_.Exception.Message)"
}

Wenn -ErrorAction Stop oder $ErrorActionPreference = 'Stop' wirksam ist, verpackt die Engine den nicht abschließenden Fehler in eine ActionPreferenceStopException und stuft ihn zu einem abschließenden Fehler hoch. Innerhalb eines try-Blocks ist es dieser hochgestufte Fehler, der bei catch ankommt — das ist der genaue Mechanismus dahinter.1 Demgegenüber sind Ausnahmen von .NET-Methoden (etwa [int]::Parse('abc')) und Fehler bei der Auflösung von Befehlsnamen von vornherein abschließende Fehler, sodass sie catch ohne weiteres Zutun erreichen.1

Der Gedanke „warum dann nicht einfach immer $ErrorActionPreference = 'Stop' setzen“ trifft nur halb zu. Bei einem unbeaufsichtigt laufenden Skript ist es sicherer, anzuhalten und den Fehlschlag zu melden, als den Fehler zu verschlucken und weiterzumachen, daher ist es ein guter Standard, am Anfang Stop zu setzen. Bedenken Sie jedoch, dass $ErrorActionPreference für diesen Geltungsbereich und dessen untergeordnete Bereiche gilt, wodurch sich auch das Verhalten aufgerufener Module oder Funktionen ändert, und dass Aufräumschritte, bei denen ein Fehlschlag akzeptabel ist (etwa das Löschen temporärer Dateien), wieder explizit mit -ErrorAction SilentlyContinue versehen werden müssen.2

3. Was Sie in catch auslesen sollten — Ein Rundgang durch das ErrorRecord

In einem catch-Block enthält $_ das ErrorRecord. Alles, was sich zu protokollieren lohnt, lässt sich von hier abrufen.14

try {
    Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
    # Ein typspezifisches catch behandelt nur "erwartete Fehlschläge" einzeln.
    # Auch bei einem hochgestuften Fehler gleicht die Engine gegen den ursprünglichen Ausnahmetyp ab
    Write-Warning "I/O error: $($_.Exception.Message)"
}
catch {
    # Unerwartete Fehler mit vollständigem Kontext protokollieren und erneut werfen (nicht verschlucken)
    $rec = $_   # $_ ist das ErrorRecord
    Write-Warning ('Type: {0} / Location: {1} / Target: {2}' -f `
        $rec.Exception.GetType().FullName,
        $rec.InvocationInfo.PositionMessage,
        $rec.TargetObject)
    throw       # Ein throw ohne Argument gibt denselben Fehler nach oben weiter
}
finally {
    # finally läuft unabhängig davon, ob try erfolgreich ist, einen Fehler auslöst oder mit Strg+C gestoppt wird. Aufräumarbeiten gehören hierher
    if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}

Es gibt hier drei Dinge, die es zu verstehen lohnt.

  • $_.Exception ist die Ausnahme selbst. Ein durch -ErrorAction Stop hochgestufter Fehler wird in eine ActionPreferenceStopException verpackt, aber beim Typabgleich in catch betrachtet die Engine den ursprünglichen Ausnahmetyp (etwa ItemNotFoundException), sodass Sie ein typspezifisches catch ganz normal schreiben können. Über $_.Exception.ErrorRecord gelangen Sie zurück zum ursprünglichen ErrorRecord.1
  • $_.InvocationInfo.PositionMessage verrät genau, welche Datei, welche Zeile und welcher Befehl betroffen sind — und ob Ihre Protokolle aus unbeaufsichtigten Läufen das enthalten, macht den Unterschied zwischen einer Untersuchung, die Minuten dauert, und einer, die Stunden dauert.
  • Der finally-Block läuft unabhängig davon, ob try erfolgreich ist, einen Fehler auslöst oder mit Strg+C gestoppt wird. Aufräumarbeiten wie das Schließen von Verbindungen oder das Löschen temporärer Dateien gehören in finally.7

Die Designfrage, auf welcher Ebene man abfängt und wo man protokolliert, ist sprachübergreifend universell. Die in Wo sollten catch und Protokollierung bei der Fehlerbehandlung stehen? dargelegten Prinzipien — an der Grenze abfangen, Fehler nicht verschlucken, doppelte Protokollierung vermeiden — lassen sich genauso direkt auf PowerShell übertragen.

4. Erfolg oder Fehlschlag nativer Befehle — $?, $LASTEXITCODE und die neue Funktion in 7.4

Die andere große Lücke sind native Befehle — robocopy, git, hausinterne EXE-Dateien. Externe Programme nehmen nicht am Fehlersystem von PowerShell teil; sie melden einen Fehlschlag stattdessen über einen Exit Code. So sieht das Standardverhalten aus.1

Ereignis Verhalten (Standard)
Von Null verschiedener Exit Code $? wird zu $false, und der Exit Code landet in $LASTEXITCODE
Erzeugung eines ErrorRecord Keine (wird nicht einmal zu $Error hinzugefügt)
try/catch Wird nicht ausgelöst

Mit anderen Worten: try { robocopy ... } catch { ... } fängt (standardmäßig) nichts ab. Schreiben Sie Ihre Erfolgs-/Fehlerprüfung für einen nativen Befehl gegen $LASTEXITCODE. $? ist ein boolescher Wert für „war der unmittelbar vorangegangene Vorgang erfolgreich“, und bei einem nativen Befehl ist er nur dann $true, wenn der Exit Code 0 ist.1 Beachten Sie außerdem, dass in Windows PowerShell 5.1 schon das bloße Schreiben eines nativen Befehls nach stderr $? auf $false setzen konnte; PowerShell 7 hat dies geändert, sodass $? nur bei einem von Null verschiedenen Exit Code zu $false wird — eine Änderung, die der Realität besser entspricht, da ein Schreiben nach stderr nicht gleichbedeutend mit einem Fehlschlag ist.8

# Das Ergebnis eines nativen Befehls anhand von $LASTEXITCODE beurteilen
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
    # robocopy behandelt 0-7 als erfolgsähnlich (informativ, z. B. ob überhaupt etwas kopiert wurde); ab 8 bedeutet Fehlschlag
    throw "robocopy failed (ExitCode=$LASTEXITCODE)"
}

Ab PowerShell 7.4 lässt sich dieses Verhalten mit $PSNativeCommandUseErrorActionPreference ändern. Es wurde in 7.3 als experimentelles Feature eingeführt und wurde in 7.4 zu einem regulären Feature.3 Setzen Sie es auf $true, löst ein nativer Befehl mit von Null verschiedenem Exit Code einen nicht abschließenden Fehler aus, der den Exit Code benennt, und dieser Fehler folgt $ErrorActionPreference. Kombiniert mit Stop laufen also auch Fehlschläge externer Befehle mit try/catch mit.12

# PowerShell 7.4+: Fehlschläge externer Befehle ebenfalls mit try/catch behandeln (Standard ist $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try {
    git.exe fetch origin
}
catch {
    Write-Warning "git failed: $($_.Exception.Message)"
    throw
}

& {
    # Bei einem Befehl wie robocopy, bei dem "ungleich null" nicht Fehlschlag bedeutet, dies
    # innerhalb eines Skriptblocks vorübergehend deaktivieren und wie bisher anhand von
    # $LASTEXITCODE beurteilen (wird beim Verlassen des Blocks zurückgesetzt)
    $PSNativeCommandUseErrorActionPreference = $false
    robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
    if ($LASTEXITCODE -ge 8) { throw "robocopy failed (ExitCode=$LASTEXITCODE)" }
}

Genau wie es die offizielle Dokumentation am Beispiel von robocopy selbst zeigt, verwenden manche Befehle einen von Null verschiedenen Exit Code als normale, aussagekräftige Information, sodass Sie beim breiten Einschalten dieser Funktion Ausnahmebereiche dafür einplanen müssen.2 In Umgebungen, die ausschließlich auf Windows PowerShell 5.1 festliegen, gibt es dieses Feature überhaupt nicht; vereinheitlichen Sie dort auf die Beurteilung per $LASTEXITCODE. Verhaltensunterschiede zwischen 5.1 und 7 sind genau die Art von Stolperfalle, die bei einer Migration auftritt, weshalb es sich lohnt, sie bei jeder geplanten Migration sorgfältig zu dokumentieren.

5. Exit-Code-Design — Erfolg und Fehlschlag für Aufgabenplanung und Monitoring sichtbar machen

Sobald Sie den Fehler abgefangen haben, folgt der nächste Schritt: die Meldung nach außen. In der Praxis ist der Exit Code des Prozesses praktisch der einzige Weg, auf dem die Aufgabenplanung oder ein Monitoring-Werkzeug erfährt, ob ein Skript erfolgreich war. Halten wir die genaue Spezifikation fest.

  • Mit exit <Zahl> können Sie den Exit Code des Skripts explizit festlegen. exit setzt außerdem einen Wert in $LASTEXITCODE.59
  • Beim Start mit pwsh -File (powershell.exe -File) wird der an exit übergebene Wert unverändert zum Exit Code des Prozesses. Ohne exit-Anweisung ist ein normaler Abschluss 0, und die Beendigung durch eine unbehandelte Ausnahme ist 1.56
  • Starten Sie das Skript stattdessen mit -Command, bleibt ein Exit Code wie exit 10 innerhalb des Skripts nicht erhalten. Er wird je nachdem, ob der letzte Befehl erfolgreich war, auf 0 oder 1 heruntergerundet (schreiben Sie exit 10 jedoch direkt in die Befehlszeichenkette selbst, wird dieser Wert unverändert zurückgegeben). Wenn Ihr Betrieb auf der Unterscheidung verschiedener Skript-Exit-Codes beruht, ist der Start mit -File der Standardweg.6

Setzt man diese Spezifikation in ein Grundgerüst um, sieht eine Vorlage für ein unbeaufsichtigt laufendes Skript so aus.

# Invoke-NightlyExport.ps1 — ein Grundgerüst, mit dem die Aufgabenplanung Erfolg oder Fehlschlag beurteilen kann
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'   # Bei unbeaufsichtigten Läufen "anhalten und melden" zum Standard machen

# Ein vollständiges Transcript inklusive Standardausgabe und Fehlern als Protokoll führen (mit -Append an eine Tagesdatei anhängen)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append

try {
    Export-DailyData      # Die eigentliche Fachlogik (ruft eine Funktion aus einem Modul auf)
    exit 0                # Erfolg explizit signalisieren
}
catch [System.Net.WebException] {
    Write-Warning "Communication error: $($_.Exception.Message)"
    exit 10               # Kategorie vorübergehender Fehler — lässt Raum, die Aufgabe für einen Retry zu konfigurieren
}
catch {
    Write-Warning "Unexpected error: $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1                # Dauerhafter Fehler — nicht erneut versuchen; ein Mensch muss sich das ansehen
}
finally {
    Stop-Transcript       # In finally wird das Transcript auch dann geschlossen, wenn wir über exit verlassen werden
}

Start-Transcript ist ein Cmdlet, das die gesamte Ein- und Ausgabe einer Sitzung als Text aufzeichnet, sodass Sie genau reproduzieren können, was zu diesem Zeitpunkt auf dem Bildschirm stand, ohne selbst Echo-Anweisungen oder Umleitungen einzubauen.10 Es lohnt sich, es parallel zu eigenen Protokollierungsfunktionen zu betreiben, statt beide als sich gegenseitig ausschließend zu behandeln — betrachten Sie es als letzte Verteidigungslinie. Protokolldesign und der Umgang mit wachsenden Protokollen werden in PowerShell-Skripte in der Praxis ── Protokollauswertung, Archivierung und Reporting sicher automatisieren behandelt.

Der Trick bei der Exit-Code-Vergabe ist, es nicht zu überkonstruieren. Eine Granularität von etwa 0 = Erfolg, 1 = dauerhafter Fehler (ein Mensch muss hinschauen) und die 10er-Werte = vorübergehender Fehler (Retry unbedenklich) reicht völlig aus und lässt sich direkt auf das „Letzte Ausführungsergebnis“ der Aufgabenplanung oder die Erfolgs-/Fehlerprüfung eines Job-Management-Werkzeugs abbilden. Zur Konfiguration auf Seiten der Aufgabe (erneuter Versuch bei Fehlschlag, wie man das Ergebnis prüft) siehe Wenn Aufgaben der Aufgabenplanung nicht laufen oder mit 0x1 enden — Ursache eingrenzen und zuverlässigen Betrieb gestalten.

6. Retry-Design — Vorübergehende Fehler von fachlichen Fehlern unterscheiden

Zuletzt die Retries. Der Wert eines Retry liegt darin, vorübergehende Fehler automatisch abzufedern und niemanden mitten in der Nacht aufzuwecken — wird er jedoch achtlos angeflanscht, entstehen andere Arten von Zwischenfällen: endloses erneutes Versuchen eines dauerhaften Fehlschlags oder eine Datenbeschädigung durch doppelte Verarbeitung. Es gelten drei Prinzipien.

  • Nur vorübergehende Fehler erneut versuchen. Beschränken Sie Retries auf Fehlschläge, die sich von selbst mit der Zeit lösen — eine Netzwerkunterbrechung, eine vorübergehende Dateisperre, das Warten auf den Start eines abhängigen Dienstes. Lassen Sie ungültige Eingaben, fehlende Berechtigungen und Fehlkonfigurationen sofort fehlschlagen und übergeben Sie sie per Exit Code und Protokoll an einen Menschen.
  • Eine Obergrenze für Anzahl und Intervall festlegen. Bestimmen Sie eine maximale Anzahl von Versuchen und verlängern Sie das Intervall per exponentiellem Backoff (2 Sekunden, 4 Sekunden, 8 Sekunden usw.). Ein angeschlagenes Gegenüber in festem Takt zu bombardieren, behindert dessen Erholung nur.
  • Idempotent gestalten (gefahrlos erneut ausführbar). Sowohl ein Retry als auch ein erneuter Lauf über die Aufgabenplanung bedeuten: „dieselbe Verarbeitung läuft noch einmal“. Grundvoraussetzung ist ein Design wie die Veröffentlichung der Ausgabe über eine temporäre Datei plus Umbenennung oder das Erfassen bereits verarbeiteter IDs, um eine doppelte Übernahme abzuweisen.

Als Muster läuft das auf folgende Form hinaus.

function Invoke-WithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [scriptblock] $Operation,
        # Wird 0 oder weniger übergeben, würde die Schleife erfolgreich enden, ohne je zu laufen; deshalb mindestens 1 erzwingen
        [ValidateRange(1, 100)]
        [int] $MaxAttempts = 4,
        # Ein negativer Wert würde bei einem Retry in Start-Sleep einen anderen Fehler auslösen, daher schon bei der Parameterbindung zurückweisen
        [ValidateRange(0, 3600)]
        [int] $BaseDelaySeconds = 2,
        # Nur die Ausnahmetypen auflisten, bei denen sich ein Retry lohnt (Standard sind E/A und Netzwerk)
        [Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
    )
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            # Die Ausgabe zunächst in eine Variable aufnehmen und erst nach Erfolg zurückgeben. Würde & $Operation
            # direkt zurückgegeben, könnte bei einer Ausnahme mittendrin eine teilweise Ausgabe an den Aufrufer
            # durchsickern, sodass ein später erfolgreicher Retry dieselben Daten doppelt liefern würde
            $output = & $Operation
            return $output
        }
        catch {
            $ex = $_.Exception
            $isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
            if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
                throw   # Ein fachlicher Fehler, oder das Retry-Limit ist erreicht — unverändert fehlschlagen lassen
            }
            # Die exponentiell wachsende Wartezeit deckeln (damit eine Konfiguration mit vielen Versuchen
            # nicht zu lange wartet und den von Start-Sleep akzeptierten Bereich nicht überschreitet)
            $delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
            Write-Warning "Failed (attempt ${attempt}): $($ex.Message) — retrying in ${delay} seconds"
            Start-Sleep -Seconds $delay
        }
    }
}

# Verwendung: Stellen Sie sicher, dass der Zielvorgang mit -ErrorAction Stop zu einem abschließenden Fehler wird
Invoke-WithRetry -Operation {
    Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}

# Ein Vorbehalt beim erneuten Versuchen von Invoke-RestMethod / Invoke-WebRequest unter PowerShell 7:
# Unter 7 kommt ein Kommunikationsfehler als Typ aus der HttpRequestException-Familie an statt
# als WebException wie zu 5.1-Zeiten, sodass er mit den obigen Standardwerten nicht erneut versucht
# wird. Außerdem kommt eine dauerhafte HTTP-Fehlerantwort wie 404 mit demselben Typ an, sodass Sie
# nach Eintreffen einer Antwort selbst anhand des Statuscodes entscheiden müssen, ob sich ein Retry lohnt
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
    # -SkipHttpErrorCheck liefert auch eine Fehlerantwort, ohne zu werfen, sodass wir den Code prüfen und selbst entscheiden können, wie geworfen wird
    $r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
    if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
        # Nur die vorübergehenden Codes als HttpRequestException werfen → wird erneut versucht
        throw [System.Net.Http.HttpRequestException]::new("Transient HTTP error: $($r.StatusCode)")
    }
    if ($r.StatusCode -ge 400) {
        throw "Permanent HTTP error: $($r.StatusCode)"   # anderer Typ, wird daher nicht erneut versucht
    }
    $r.Content | ConvertFrom-Json
}

Der entscheidende Punkt ist, dass explizit nach Typ ausgewählt wird, welche Ausnahmen erneut versucht werden. Schreiben Sie stattdessen „alles erneut versuchen, was abgefangen wird“, warten Sie selbst bei einem dauerhaften Fehler wie einem falschen Parameter sinnlos vier Versuche lang. Nach der Inbetriebnahme ist ein realistischer Weg, dies weiterzuentwickeln, $RetryableExceptions um die vorübergehenden Fehlertypen zu ergänzen, die Sie tatsächlich in den Protokollen beobachten. Es lohnt sich, einen solchen gemeinsamen Helfer in ein wiederverwendbares Modul auszulagern. Retry-Logik und Fehlerverzweigungen sind zudem genau der Bereich, für den sich Pester-Tests lohnen (siehe PowerShell-Tests mit Pester — Ein praktischer Ansatz, um Betriebsskripte robuster gegen Fehler zu machen).

7. Praktische Faustregeln (Entscheidungstabelle)

Fragestellung Optionen Faustregel
Standard-Fehlerverhalten Bei Continue belassen / $ErrorActionPreference = ‘Stop’ am Anfang setzen Unbeaufsichtigte Läufe fahren sicherer mit „anhalten und melden“. Ein interaktives Untersuchungsskript kann bei Continue bleiben2
Wo Sie abfangen möchten Auf das Beste hoffen / -ErrorAction Stop explizit hinzufügen Cmdlets lösen meist nicht abschließende Fehler aus. Fügen Sie Stop explizit bei jeder Zeile hinzu, die Sie abfangen möchten1
Erfolg/Fehlschlag nativer Befehle Ignorieren / Anhand von $LASTEXITCODE beurteilen / $PSNativeCommandUseErrorActionPreference aus 7.4 In Umgebungen mit 5.1-Mischbetrieb auf $LASTEXITCODE vereinheitlichen. Bei ausschließlich 7.4+ das neue Feature plus Ausnahmebereiche für Fälle wie robocopy nutzen32
Erfolg/Fehlschlag nach außen melden Nur Protokolle / Exit Codes entwerfen und mit -File starten Protokolle sind für Menschen, Exit Codes sind für Maschinen — Sie brauchen beides. Ein Start mit -Command glättet den Exit Code6
Ausführungsprotokoll Nur eigene Protokollierung / Zusätzlich Start-Transcript verwenden Ein Sicherheitsnetz, das Ausgaben erfasst, die die eigene Protokollierung nicht erfasst (z. B. die Standardausgabe eines externen Befehls)10
Retries Jeden Fehler erneut versuchen / Auf vorübergehende Fehler beschränken + exponentielles Backoff + Idempotenz Ein Retry bei einem fachlichen Fehler ist ein Unfall, der nur auf seinen Moment wartet. Obergrenze, Intervall und Idempotenz kombinieren

8. Zusammenfassung

  • PowerShell-Fehler gliedern sich in nicht abschließende und abschließende Fehler; nicht abschließende Fehler erreichen try/catch standardmäßig nicht. Der Standardweg zur Abhilfe ist, jedem Befehl, den Sie abfangen möchten, explizit -ErrorAction Stop hinzuzufügen.
  • $ErrorActionPreference ist standardmäßig auf Continue gesetzt. Setzen Sie $ErrorActionPreference eines unbeaufsichtigten Skripts am Anfang auf Stop, um strukturell den Unfall zu verhindern, einen Fehlschlag zu verschlucken und zu beenden, als wäre alles erfolgreich gewesen.
  • Der Fehlschlag eines nativen Befehls erreicht catch standardmäßig nicht. Beurteilen Sie ihn mit $LASTEXITCODE, oder setzen Sie ab PowerShell 7.4 $PSNativeCommandUseErrorActionPreference ein.
  • Protokollieren Sie in catch Typ, Meldung und Position der Ausnahme aus $_ (dem ErrorRecord), und legen Sie Aufräumarbeiten in finally ab. finally läuft auch bei Strg+C oder einem exit.
  • Melden Sie Erfolg oder Fehlschlag über den Exit Code nach außen. Beim Start mit -File wird der an exit übergebene Wert unverändert zum Exit Code des Prozesses, sodass Aufgabenplanung oder Monitoring Erfolg von Fehlschlag unterscheiden können.
  • Retries folgen drei Prinzipien: beschränkt auf vorübergehende Fehler, gedeckeltes exponentielles Backoff und Idempotenz. Lassen Sie dauerhafte Fehler sofort fehlschlagen und übergeben Sie sie an einen Menschen.

Verwandte Artikel

Verwandte Beratungsbereiche

Die KomuraSoft LLC übernimmt die Überprüfung von Fehlerbehandlung und Retry-Design für nächtliche Batch-Jobs und Routineskripte, die Untersuchung intermittierender Fehler wie „als Erfolg gemeldet, obwohl es tatsächlich fehlgeschlagen ist“ oder „fällt nur einmal im Monat aus“, sowie die Verbesserung der Betriebsqualität bestehender Skript-Assets.

  1. Microsoft Learn, about_Error_Handling. Zu den drei Fehlerkategorien (nicht abschließend, anweisungsabschließend, skriptabschließend), dazu, dass ein nicht abschließender Fehler standardmäßig catch/trap nicht erreicht, zum Hochstufungsmechanismus über -ErrorAction Stop (ActionPreferenceStopException und $_.Exception.ErrorRecord), dazu, dass ein typspezifisches catch gegen den ursprünglichen Ausnahmetyp abgleicht, zur Semantik von $? und $LASTEXITCODE, dazu, dass ein von Null verschiedener Exit Code eines nativen Befehls standardmäßig kein ErrorRecord erzeugt, sowie zum Verhalten von $PSNativeCommandUseErrorActionPreference.  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  2. Microsoft Learn, about_Preference_Variables. Dazu, dass $ErrorActionPreference standardmäßig auf Continue steht, dass der Parameter -ErrorAction für einen einzelnen Befehl Vorrang hat, dass die Einstellung für ihren Geltungsbereich und dessen untergeordnete Bereiche gilt, dass $PSNativeCommandUseErrorActionPreference standardmäßig $false ist, sowie zum Beispiel des vorübergehenden Deaktivierens innerhalb eines Skriptblocks für einen Befehl wie robocopy, der einen von Null verschiedenen Exit Code als Information nutzt.  2 3 4 5 6 7

  3. Microsoft Learn, What’s New in PowerShell 7.4. Dazu, dass das experimentelle Feature PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) in PowerShell 7.4 zu einem regulären Feature wurde.  2 3

  4. Microsoft Learn, Everything you wanted to know about exceptions. Zum Zugriff auf Ausnahmeinformationen über $_ innerhalb eines catch-Blocks, dazu, dass sowohl ein Befehl mit -ErrorAction Stop als auch ein Write-Error-Fehler in catch behandelbar werden, sowie zum Muster der Ressourcenfreigabe über try/finally.  2

  5. Microsoft Learn, about_Language_Keywords. Dazu, dass das Schlüsselwort exit den Exit Code setzt und diesen auch in $LASTEXITCODE widerspiegelt, dass ein mit pwsh -File gestartetes Skript das numerische Argument von exit als Exit Code zurückgibt, sowie dazu, dass es bei fehlender exit-Anweisung normal mit 0 oder bei einer unbehandelten Ausnahme mit 1 abschließt.  2 3

  6. Microsoft Learn, about_Pwsh. Dazu, wie der Exit Code bei einem Start mit -File bestimmt wird, sowie dazu, dass ein Start mit -Command jeden Exit Code außer 0 oder 1 in 1 umwandelt, weshalb exit $LASTEXITCODE nötig ist, um ihn zu erhalten.  2 3 4

  7. Microsoft Learn, about_Try_Catch_Finally. Zur Syntax von try/catch/finally, zu typspezifischen und mehrfachen catch-Blöcken sowie dazu, dass ein finally-Block bei Erfolg, bei einem Fehler, bei einem Stopp mit Strg+C und sogar bei einem exit innerhalb von catch läuft. 

  8. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Dazu, wie PowerShell 7 das Verhalten geändert hat, sodass $? nicht mehr allein dadurch $false wird, dass ein nativer Befehl nach stderr schreibt, sondern nur noch bei einem von Null verschiedenen Exit Code. 

  9. Microsoft Learn, about_Automatic_Variables. Dazu, dass $LASTEXITCODE den Exit Code eines nativen Programms oder Skripts enthält, sowie dazu, wie es beim Aufruf über pwsh -File bei einer Ausnahme auf 1, auf den Wert des exit-Schlüsselworts oder bei normalem Abschluss auf 0 gesetzt wird. 

  10. Microsoft Learn, Start-Transcript. Zum Aufzeichnen der Befehle und der Konsolenausgabe einer Sitzung in einer Textdatei, zum Anhängen mit -Append, zum Standardspeicherort und Dateinamen sowie zum Stoppen mit Stop-Transcript.  2

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Ich habe ein try/catch in PowerShell geschrieben, aber warum wird der catch-Block nie erreicht?
Weil die meisten Fehler, die Cmdlets auslösen, nicht abschließende Fehler (non-terminating errors) sind. try/catch fängt nur abschließende Fehler ab; ein nicht abschließender Fehler zeigt eine Meldung an und lässt die Pipeline weiterlaufen, sodass er catch nie erreicht. Der Standardweg zur Abhilfe ist, dem Befehl, den Sie abfangen möchten, -ErrorAction Stop hinzuzufügen (oder am Anfang des Skripts $ErrorActionPreference = 'Stop' zu setzen). Dadurch wird der nicht abschließende Fehler zu einem abschließenden Fehler hochgestuft, sodass try/catch ihn behandeln kann.
Wie entscheide ich zwischen $? und $LASTEXITCODE?
$? ist ein boolescher Wert, der anzeigt, ob der unmittelbar vorangegangene Vorgang erfolgreich war; er wird sowohl für Cmdlets als auch für native Befehle gesetzt. $LASTEXITCODE ist der Exit Code des zuletzt ausgeführten nativen Programms (oder des zuletzt mit exit beendeten Skripts) und ändert sich bei Cmdlet-Fehlern nicht. Um zu beurteilen, ob ein externer Befehl wie robocopy oder git erfolgreich war, ist $LASTEXITCODE die verlässliche Wahl, da Sie zusätzlich prüfen können, was der jeweilige Exit Code konkret bedeutet. Beachten Sie, dass ein von Null verschiedener Exit Code eines nativen Befehls standardmäßig nicht in einem catch-Block landet.
Wie kann die Aufgabenplanung beurteilen, ob ein PowerShell-Skript erfolgreich war oder fehlgeschlagen ist?
Legen Sie am Ende des Skripts (und in jedem catch-Block) mit dem exit-Schlüsselwort einen expliziten Exit Code fest, und lassen Sie die Aufgabe das Skript mit pwsh -File (oder powershell.exe -File) starten, damit Sie den Wert „Letztes Ausführungsergebnis“ überwachen können. Beim Start mit -File wird der an exit übergebene Wert unverändert zum Exit Code des Prozesses; ohne exit-Anweisung liefert ein normaler Abschluss 0 und eine unbehandelte Ausnahme 1. Ein Start mit -Command reduziert dagegen jeden Exit Code außer 0 und 1 auf 1. Wenn Sie den Betrieb auf Exit Codes aufbauen wollen, ist der Start mit -File daher der Standardweg.
Bei welchen Fehlern sollte man einen erneuten Versuch (Retry) durchführen?
Beschränken Sie Retries auf vorübergehende Fehler, bei denen ein erneuter Versuch das Ergebnis tatsächlich ändern könnte — eine kurze Netzwerkunterbrechung, eine vorübergehend gesperrte Datei, das Warten auf den Start eines abhängigen Dienstes und Ähnliches. Fachliche Fehler und dauerhafte Fehler wie ungültige Eingabedaten, fehlende Berechtigungen oder Fehlkonfigurationen schlagen unabhängig von der Anzahl der Versuche immer wieder fehl; lassen Sie diese stattdessen sofort fehlschlagen und informieren Sie über Protokolle und den Exit Code einen Menschen. Auch wenn Sie einen Retry durchführen, brauchen Sie eine Obergrenze für Anzahl und Intervall, eine Verlängerung der Intervalle per exponentiellem Backoff sowie die grundlegende Voraussetzung, dass die Verarbeitung idempotent gestaltet ist, damit ein erneuter Lauf keine doppelte Verarbeitung verursacht.
Was bewirkt die Einstellung $PSNativeCommandUseErrorActionPreference in PowerShell 7.4?
Es ist eine Einstellung, die einen PowerShell-Fehler (einen nicht abschließenden Fehler) auslöst, wenn ein nativer Befehl mit einem von Null verschiedenen Exit Code endet. Sie wurde in PowerShell 7.3 als experimentelles Feature eingeführt und wurde in 7.4 zu einem regulären Feature (der Standardwert ist $false). Setzen Sie sie auf $true, folgen Fehlschläge nativer Befehle $ErrorActionPreference; kombiniert mit Stop können Sie Fehlschläge externer Befehle also mit try/catch abfangen. Allerdings verwenden manche Befehle wie robocopy von Null verschiedene Exit Codes als normale, aussagekräftige Information, sodass Sie darauf achten müssen, die Einstellung rund um solche Aufrufe vorübergehend wieder auf $false zu setzen.

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.

Zurück zum Blog