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

$ErrorPreference ist eine der von PowerShell vordefinierten Variablen, über die das Standardverhalten bei Fehler, Warnung etc angepasst werden kann:

PS C:\> Get-Variable *pref*

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

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.

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, wenn Sie die $ErrorActionPreference gesetzt haben.

$ErrorActionpreference = "silentlycontinue"
$error.clear()
write-host "einen Fehler machen"
$a=1/0
if ($error) {
   write-host "Fehler gefunden
   $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 geworden der in $error erscheint.

Error-Handling mit Trap, Throw und Try/Catch/Finally

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. 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.

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 "Hier sollte dann die Fehlerbehandlung stehen"
}
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.

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.

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 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 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