PowerShell Error Handling

Software tut immer genau das, was man codiert hat. Die meisten Fehler sind durch den Entwickler verursacht und meist nicht dem Computer anzulasten. Aber auch das eigene Programm ist abhängig von anderen Faktoren. Und das kann schon mal knirschen, z. B. weil andere Server oder Dienste nicht erreichbar sein oder die Daten nicht im erwarteten Format vorliegen. Wenn Sie nicht wollen, dass ihr Skript einfach abbricht, sollten Sie eine Fehlerbehandlung einbauen.

$ErrorActionPreference u.a.

Über globale Variablen in der PowerShell können Sie das Standardverhalten bei Fehler, Warnung etc. anpassen

PS C:\> Get-Variable *pref*

Name                           Value
----                           -----
ConfirmPreference              High
DebugPreference                SilentlyContinue
ErrorActionPreference          Stop
ProgressPreference             Continue
VerbosePreference              SilentlyContinue
WarningPreference              Continue
WhatIfPreference               False

Mögliche Werte für "$ErrorActionPreference" sind:

Stop              Ausführung sofort beenden
Continue          Anzeigen aber weiter machen
SilentlyContinue  nicht Anzeigen und weiter machen. Sollten Sie nur mit eigener Fehlerbehandlung machen
Inquire           Rückfrage, wie damit umgegangen werden soll. Skript wird solange angehalten!
Ignore            Ignorieren aber ist nicht mit $ErrorActionPreference sondern nur im CMDLet-Parameter nutzbar
Suspend           Pausiert PowerShell Workflows. ist nicht mit $ErrorActionPreference sondern nur im CMDLet-Parameter nutzbar
Break             Startet den Debugger

Die anderen Variablen haben ggfls. noch andere Optionen. Damit ein Fehler ihr Skript nicht abbrechen lässt, können Sie die $ErrorActionPreference natürlich einfach auf "Continue" oder sogar "SilentlyContinue" stellen. Damit werden Fehler einfach ignoriert.

Achtung: Es gibt kritische Fehler, die dennoch ihr Skript ungeplant beenden. Diese können sie dann nur mit Try/Catch-Anweisungen abfangen

Bitte stellen Sie diesen Wert nur dann um, wenn Sie eine Fehlerbehandlung im Code hinterlegt haben. Denn wenn etwas nicht korrekt funktioniert, können die Folgeaktionen nicht nur fehl schlagen und die erwünschte Aktion nicht durchgeführt werden. Es kann auch richtig nach hinten losgehen und weiterer Schaden entstehen.

$?, $error

Eine einfache Möglichkeit einer Fehlerbehandlung ist natürlich die Abfrage der Fehlervariable. Dort landen die Meldungen auch, wenn Sie ein "$ErrorActionPreference = silentlycontinue" gesetzt haben.

  • $? = Status des letzten Befehls.
    Kann nur einmal abgefragt werden, weil die Abfrage abfrage selbst auch schon wieder eine erfolgreiche Funktion
    True = Erfolgreich
    False = Fehler
  • $error = Array mit einer Liste der letzten Fehler
    Wenn kein Fehler vorhanden ist, ist die Variable leer, d.h. ein Array mit "0" Elementen. Sie wird nicht beim Auslesen gelöscht!
$ErrorActionpnreference = "silentlycontinue"
$error.clear()
write-host "Fehler mit DIV0 provozieren"
$a=1/0
if ($?) {
   write-host "Fehler gefunden"
} if ($?) {
   write-host "Kein Fehler mehr"
} write-host "Letzter Fehler $($error[0])"
$error.clear()

Denken Sie daran, die Variable $Error nach der Fehlerbehandlung zu leeren, da sonst jede weitere Abfrage danach auf "$error" wieder zutrifft, auch wenn kein neuer Error dazu gekommen ist. Manchmal ist der Fehler aber gar nicht direkt bei der Ausführung einer Aktion sondern auch im Code selbst. Wenn sie z.B. auf eine Variable zugreifen, die noch nie initialisiert wurde, aber sie mit "SET PSDEUG -strict" eine Variableninitialisierung vor der ersten Verwendung erzwingen, dann wird auch dafür ein Fehler in $error hinterlegt.

$LASTEXITCODE

Wenn Sie aus der PowerShell eigene Prozess starten, z.B. eine EXE-Datei, und diese dann einen Exitcode zurück übergibt, dann finden Sie diesen Werten nicht zwingend ín $? oder $error, da der Prozess ja gestartet werden konnte. Voraussetzung ist aber, dass das aufgerufene Programm auch einen Exitcode übergibt, z.B. in dem am Ende ein "EXIT <nummer>" ausführt.

Der EXITCode ist quasi eine Altlast und Kompatibilität zu MSDOS, wo aufgerufene Programme abhängig vom Ergebnis unterschiedliche Exitcodes geliefert haben und ein Admin damit erkennen konnte, wie er die anderweitig bereitgestellten Ergebnisse verwenden kann.

$Error löschen

Wenn ein Skript sich selbst um seine Error-Behandlung kümmert, d.h. die Fehler mit Try/Catch abfängt oder mit "ErrorPreference" ein Continue oder SilentlyContinue vorgibt, dann landen die Error dennoch in der Variable $error. Oft ist es aber so, dass man die Fehler, die man schon abgefangen hat, hier gar nicht mehr sehen will. Generell ist ja eine "0-Error-Strategie" für Programmierer der beste Weg. Die Variable $error kann natürlich einfach komplett geleert werden.

# Error-Speicher leeren
$error.clear()

Wer aber nur die letzte Meldung löschen möchte, kann die ebenfalls erreichen.

# Error-Speicher leeren
$Error.Remove($error[$Error.Count-1])
# liefert selbst keinen Fehler, wenn $error null ist.
#Alternativ:
$error.removerange(0,1)
# liefert aber selbst einen Error, wenn nichts mehr drin ist

Wenn man so einen Fehler mit Try/Catch abfängt und löscht, dann sollte außerhalb dieser Bereiche die Variable "$Error" natürlich leer sein. Eine gelegentliche Anfrage darauf hilft natürlich auch beim Fehlersuchen. Bei automatisierten Prozessen können die verbliebenenn Fehler z.B. in eine Protokolldatei geschrieben oder per Mail versendet werden.

$Error mit 256 Fehlern

PowerShell protokolliert im Gegensatz zu VBScript bis zu 256 Fehler in der eigens dazu bestimmten Variable $ERROR mit. Daher gibt es auch die Konstruktion "on error resume" nicht mehr. Statt dessen kann man bei diversen Commandlets eine Option "-ErrorVariable" und "-ErrorAction" mitgeben um Fehlerausgaben und Abbruchverhalten individuell zu bestimmten. Das hilft aber nicht viel weiter, wenn man in einem Skript einen Aufruf tätigt, der einen Fehler wirft, z.B.

$User = [adsi]"LDAP://cn=administrator,cn=Users,dc=msxfaq,dc=test"
$User.get("FeldGibtesnicht")

Dabei ist es egal, ob es das Feld nicht gibt, oder einfach nicht gefüllt ist. Leider liefert die PowerShell kein "Leer" oder "$null" zurück, sondern eine Fehlermeldung, die auf einer Konsole immer unschön aussieht. Ich habe dazu eigentlich nur folgende Lösung, auch wenn man damit globale Einstellungen verbiegt.

$User = [adsi]"LDAP://cn=administrator,cn=Users,dc=msxfaq,dc=test"
$ErrorActionPreference = "SilentlyContinue"
$error[0]=""
$wert="" # sicherheitshalber initialisieren
$wert=$User.get("Feldnichtbelegt")
$ErrorActionPreference = "Continue"
if($error[0]-ne "") {
    write-host "Fehler aufgetreten" }

Mit "$?" kann immer auf den letzten Fehler zugegriffen werden.

Error-Handling mit Try/Catch/Finally

Alternativ kann man auch etwas wie "TRY" und "CATCH" programmieren, bei denen im TRY-Block etwas ausgeführt wird und bei einem Fehler der "CATCH"-Block dies wieder beseitigen kann. Der Finally-Block wird immer ausgeführt. Ein mit "Try" eingefasster Block wird auch nicht über $ErrorPreference behandelt, sondern ist immer "still".

try {
   write-host "Hier wird was versucht, was schief gehen kann, z.B. Division durch 0"
   $a = 1/0
}
catch {
   write-host "Fehler abgefangen: `r`n $_.Exception.Message"
}
finally {
  write-host "Finalize wird immer ausgeführt"
}
# Der Fehler wird zusaetzlich auch in $error hinterlegt
write-host $Error

Die Catch-Blöcke können mehrfach angelegt werden, um unterschiedliche Fehlersituationen gezielt abzufangen. Allerdings sollten Sie Try/Catch-Konstruktionen nicht verschachteln.

Der Sinn solcher Konstruktionen erläutere ich am besten mit einem Vergleich, wie Sie solche Fehler ansonsten abfangen würden. Zum einen finde ich Try/Catch viel lesbarer und übersichtlicher.

Das größere Problem bei der Arbeit mit $error ist aber, dass Sie die $errorpreference erst auf Continue/SilentlyContinue stellen müssen und zur Sicherheit vorher und hinterher die Variable leeren, damit die Logik "zuverlässig" ist. Alles nicht sehr schön. 

Achtung:
Wenn Sie im "CATCH"-Block die Variable $_ verwenden, dann enthält diese dort den Fehler und nicht die Daten der Schleife, die sie gerade über eine Pipeline durchlaufen. Wenn Sie in der CATCH-Abhandlung als Daten der Pipeline ausgeben wollen, müssen Sie diese vorher einer anderen Variable zuweisen.

Achtung:
Try/Catch fängt nur Fehler an, die das Skript beenden würden (Terminating errors). Es fängt keine Fehler ab, die zwar eine rote Meldung generieren aber nicht so kritisch sind, den weiteren Ablauf zu unterbrechen. Sie sollten also auf jeden Fall auch eine Behandlung für diesen Fall einbauen, z.B. wenn ein Aufruf kein Ergebnis liefert.
Why does catch not catch? https://blogs.technet.microsoft.com/bshukla/2011/07/13/why-does-catch-not-catch/

Wenn ich meinen Entwicklern glauben darf, sollte man diese Konstruktionen nicht übermäßig benutzen sondern eher um noch nicht behandelte Fehler zu erkennen. Besser ist vorab eine saubere Validierung der Werte um Fehler zu vermeiden.

Error-Handling mit Trap, Throw

Alternativ kann PowerShell natürlich auch mit TRAP und THROW arbeiten. Auf http://huddledmasses.org/trap-exception-in-PowerShell/ finden Sie eine gute Beschreibung, weil dieser Bereich in der Microsoft Dokumentation wohl ziemlich vergessen wurde.

Write-Host "Beispiel trapThrow startet"
trap [Exception] { 
   write-host
   write-error $("Fehler aufgetreten:" + $_.Exception.GetType().FullName); 
   write-error $("Fehler aufgetreten:" + $_.Exception.Message); 
   continue;
}

Write-Host "Beispiel trapThrow dazwischen1"
throw (new-object IO.DirectoryNotFoundException); write-host "Fehler1"; 
Write-Host "Beispiel trapThrow beendet"

So kann man für einen Code-Teil eine Ausnahmebehandlung hinterlegen.

Error an aufrufende Programme

Insbesondere wenn z.B. der Taskplaner ein PowerShell-Script startet oder andere Programme in PowerShell-Script starten, dann sollte das Skript bei einem Fehler unbedingt auch einen Exit-Code an das aufrufende Programm zurück geben. Das ist der einfachste und sicherste Weg, dass das aufrufende Programm auf einen Fehler reagieren kann. Innerhalb der gleichen PowerShell Umgebung ist die Variable "$error" global, d.h. kann von jedem ausgelesen werden.

Per Default macht PowerShell das aber nicht, d.h. wenn ein Script mit einem Fehler abbricht, dann ist der "Errorlevel" immer noch 0. Selbst ein "Throw" mit einem Fehler setzt nicht den Exitcode. Ich habe es mir daher zur Angewohnheit gemacht, eine eigene "Exitroutine" zu nutzen und die Fehlerbehandlung komplett selbst zu machen. Dann kann ich bei einem Fehler einfach das Skript sauber mit einem "EXIT <code>" verlassen. Ich muss allerdings dabei selbst dafür sorgen, dass ein Fehler auch behandelt wird. Hier ein Beispiel der Error-Routine:

# zuerst setze ich strict und dass Fehler nicht zum Abbruch führen und bestehende Fehler gecleared werden
Set-PSDebug -strict
#$ErrorActionPreference = "SilentlyContinue"
$ErrorActionPreference = "Continue"
$error.clear()

# dann hier eine Funktion, die ich spaeter immer wieder aufrufe
function exitonerror  {
   param (
      [int]$eventID = 0 ,
      [string]$message = "no message given", 
      [boolean]$always =$false  # allow to force exit
   )
   if ($error -or $always) {
      write-eventlog `
         -entrytype Error `
         -logname Application `
         -category 0 `
         -eventID $eventID `
         -Source 'PowerShell' `
         -Message $message
      write-error "ExitOnError: EventID=$eventid  Message=$message"
      stop-transcript
      exit $eventid
   }
}


# im Code füge ich dann an den kritischen Stellen eintweder einfach folgendes ein
exitonerror -eventid 1 -message "beschreibung des Fehlers" 

#Oder binde das in Try-Catch Routinen ein
try {
   $config=([xml](get-content -path $configxml)).config
}
catch {
   write-host ("csv2ex:ConfigXML: Failed loading" +$error)
   exitonerror -eventid 2 -message ("Unable to Load:" + $error)
}

Wer es noch universeller haben möchte kann als Source noch ein "([string]($MyInvocation.MyCommand.name))" ersetzen. Dann wird der Skriptname als Quelle genutzt.

Über den Text und insbesondere die EventID kann ich zum Skript dann sehr schnell auch den Code finden. Die Error-Funktion selbst sorgt dafür, dass der Fehler auch im Eventlog landet, ein eventuell gestartetes Transcript gestoppt und dann das Skript mit EXIT und dem Fehlercode beendet wird.

Weitere Links