Allt du ville veta om undantag
Felhantering är bara en del av livet när det gäller att skriva kod. Vi kan ofta kontrollera och validera villkor för förväntat beteende. När det oväntade inträffar går vi över till undantagshantering. Du kan enkelt hantera undantag som genereras av andra personers kod eller så kan du generera egna undantag som andra kan hantera.
Kommentar
Den ursprungliga versionen av den här artikeln visades på bloggen skriven av @KevinMarquette. PowerShell-teamet tackar Kevin för att ha delat det här innehållet med oss. Kolla in hans blogg på PowerShellExplained.com.
Grundläggande terminologi
Vi måste ta upp några grundläggande termer innan vi går in i den här.
Undantag
Ett undantag är som en händelse som skapas när normal felhantering inte kan hantera problemet. Att försöka dividera ett tal med noll eller få slut på minne är exempel på något som skapar ett undantag. Ibland skapar författaren till den kod som du använder undantag för vissa problem när de inträffar.
Kasta och fånga
När ett undantag inträffar säger vi att ett undantag utlöses. Om du vill hantera ett undantag som genereras måste du fånga det. Om ett undantag utlöses och det inte fångas av något slutar skriptet att köras.
Anropsstacken
Anropsstacken är en lista över funktioner som har anropat varandra. När en funktion anropas läggs den till i stacken eller överst i listan. När funktionen avslutas eller returneras tas den bort från stacken.
När ett undantag utlöses kontrolleras anropsstacken för att en undantagshanterare ska kunna fånga den.
Avslutande och icke-avslutande fel
Ett undantag är vanligtvis ett avslutande fel. Ett undantag som genereras fångas antingen eller så avslutas den aktuella körningen. Som standard genereras ett icke-avslutande fel av Write-Error
och lägger till ett fel i utdataströmmen utan att utlösa ett undantag.
Jag påpekar detta eftersom Write-Error
och andra icke-avslutande fel inte utlöser catch
.
Svälja ett undantag
Det här är när du får ett fel bara för att förhindra det. Gör detta med försiktighet eftersom det kan göra felsökningsproblem mycket svåra.
Grundläggande kommandosyntax
Här är en snabb översikt över den grundläggande undantagshanteringssyntaxen som används i PowerShell.
Kasta
För att skapa en egen undantagshändelse utlöser vi ett undantag med nyckelordet throw
.
function Start-Something
{
throw "Bad thing happened"
}
Detta skapar ett körningsundundatag som är ett avslutande fel. Den hanteras av en catch
i en anropande funktion eller avslutar skriptet med ett meddelande som detta.
PS> Start-Something
Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Bad thing happened:String) [], RuntimeException
+ FullyQualifiedErrorId : Bad thing happened
Write-Error -ErrorAction Stop
Jag nämnde att Write-Error
inte utlöser ett avslutande fel som standard. Om du anger -ErrorAction Stop
Write-Error
genererar ett avslutande fel som kan hanteras med en catch
.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
Tack till Lee Dailey för att du påminde om att använda -ErrorAction Stop
det här sättet.
Cmdlet - ErrorAction Stop
Om du anger -ErrorAction Stop
för en avancerad funktion eller cmdlet omvandlas alla Write-Error
instruktioner till avslutande fel som stoppar körningen eller som kan hanteras av en catch
.
Start-Something -ErrorAction Stop
Mer information om parametern ErrorAction finns i about_CommonParameters. Mer information om variabeln finns i $ErrorActionPreference
about_Preference_Variables.
Prova/fånga
Det sätt på vilket undantagshantering fungerar i PowerShell (och många andra språk) är att du först try
ett kodavsnitt och om det utlöser ett fel kan catch
du det. Här är ett snabbexempel.
try
{
Start-Something
}
catch
{
Write-Output "Something threw an exception"
Write-Output $_
}
try
{
Start-Something -ErrorAction Stop
}
catch
{
Write-Output "Something threw an exception or used Write-Error"
Write-Output $_
}
Skriptet catch
körs bara om det finns ett avslutande fel. Om körs try
korrekt hoppar den över catch
. Du kan komma åt undantagsinformationen catch
i blocket med hjälp av variabeln $_
.
Prova/slutligen
Ibland behöver du inte hantera ett fel, men du behöver fortfarande lite kod för att köra om ett undantag inträffar eller inte. Ett finally
skript gör exakt det.
Ta en titt på det här exemplet:
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
När du öppnar eller ansluter till en resurs bör du stänga den. Om utlöser ExecuteNonQuery()
ett undantag stängs inte anslutningen. Här är samma kod i ett try/finally
block.
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
I det här exemplet stängs anslutningen om det uppstår ett fel. Den stängs också om det inte finns något fel. Skriptet finally
körs varje gång.
Eftersom du inte fångar undantaget sprids det fortfarande upp i anropsstacken.
Prova/fånga/slutligen
Det är helt giltigt att använda catch
och finally
tillsammans. För det mesta använder du det ena eller det andra, men du kan hitta scenarier där du använder båda.
$PSItem
Nu när vi har grunderna ur vägen kan vi gräva lite djupare.
catch
I blocket finns en automatisk variabel ($PSItem
eller $_
) av typen ErrorRecord
som innehåller information om undantaget. Här är en snabb översikt över några av de viktigaste egenskaperna.
I de här exemplen använde jag en ogiltig sökväg i ReadAllText
för att generera det här undantaget.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
Detta ger dig det renaste meddelandet som ska användas i loggning och allmänna utdata. ToString()
anropas automatiskt om $PSItem
placeras i en sträng.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
Den här egenskapen innehåller ytterligare information som samlats in av PowerShell om funktionen eller skriptet där undantaget utlöstes. Här är InvocationInfo
från exempelfelet som jag skapade.
PS> $PSItem.InvocationInfo | Format-List *
MyCommand : Get-Resource
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 5
OffsetInLine : 5
ScriptName : C:\blog\throwerror.ps1
Line : Get-Resource
PositionMessage : At C:\blog\throwerror.ps1:5 char:5
+ Get-Resource
+ ~~~~~~~~~~~~
PSScriptRoot : C:\blog
PSCommandPath : C:\blog\throwerror.ps1
InvocationName : Get-Resource
Den viktiga informationen här visar ScriptName
kodens , Line
och ScriptLineNumber
var anropet startade.
$PSItem.ScriptStackTrace
Den här egenskapen visar ordningen på funktionsanrop som tog dig till koden där undantaget genererades.
PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18
Jag gör bara anrop till funktioner i samma skript, men det skulle spåra anropen om flera skript var inblandade.
$PSItem.Exception
Det här är det faktiska undantaget som utlöstes.
$PSItem.Exception.Message
Det här är det allmänna meddelandet som beskriver undantaget och är en bra startpunkt vid felsökning. De flesta undantag har ett standardmeddelande men kan också ställas in på något anpassat när undantaget utlöses.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Det här är också meddelandet som returnerades när det anropades $PSItem.ToString()
om det inte fanns någon uppsättning på ErrorRecord
.
$PSItem.Exception.InnerException
Undantag kan innehålla inre undantag. Detta är ofta fallet när koden du anropar fångar ett undantag och genererar ett annat undantag. Det ursprungliga undantaget placeras i det nya undantaget.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
Jag kommer att återkomma till detta senare när jag talar om återkastning av undantag.
$PSItem.Exception.StackTrace
Det här är StackTrace
för undantaget. Jag visade en ScriptStackTrace
ovan, men den här är för anrop till hanterad kod.
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )
Du får bara den här stackspårningen när händelsen genereras från hanterad kod. Jag anropar en .NET Framework-funktion direkt så det är allt vi kan se i det här exemplet. När du tittar på en stackspårning letar du vanligtvis efter var koden stoppas och systemanropen börjar.
Arbeta med undantag
Det finns fler undantag än de grundläggande syntax- och undantagsegenskaperna.
Fånga skrivna undantag
Du kan vara selektiv med de undantag som du fångar. Undantag har en typ och du kan ange vilken typ av undantag du vill fånga.
try
{
Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: $path"
}
Undantagstypen kontrolleras för varje catch
block tills ett hittas som matchar ditt undantag.
Det är viktigt att inse att undantag kan ärva från andra undantag. I exemplet ovan FileNotFoundException
ärver från IOException
. Så om var IOException
först, skulle det bli anropat istället. Endast ett catch-block anropas även om det finns flera matchningar.
Om vi hade en System.IO.PathTooLongException
, IOException
skulle matcha men om vi hade en InsufficientMemoryException
då ingenting skulle fånga det och det skulle sprida upp stacken.
Fånga flera typer samtidigt
Det går att fånga flera undantagstyper med samma catch
instruktion.
try
{
Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: [$path]"
}
Tack Redditor u/Sheppard_Ra
för att föreslå detta tillägg.
Utlösa inskrivna undantag
Du kan utlösa inskrivna undantag i PowerShell. I stället för att anropa throw
med en sträng:
throw "Could not find: $path"
Använd en undantagsaccelerator så här:
throw [System.IO.FileNotFoundException] "Could not find: $path"
Men du måste ange ett meddelande när du gör det på det sättet.
Du kan också skapa en ny instans av ett undantag som ska genereras. Meddelandet är valfritt när du gör detta eftersom systemet har standardmeddelanden för alla inbyggda undantag.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
Om du inte använder PowerShell 5.0 eller senare måste du använda den äldre New-Object
metoden.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
Genom att använda ett skrivet undantag kan du (eller andra) fånga undantaget efter den typ som nämns i föregående avsnitt.
Skrivfel – undantag
Vi kan lägga till dessa inskrivna undantag i Write-Error
och vi kan fortfarande catch
felen efter undantagstyp. Använd Write-Error
som i följande exempel:
# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop
# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop
# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop
Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop
Sedan kan vi fånga den så här:
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
Den stora listan över .NET-undantag
Jag kompilerade en huvudlista med hjälp av Reddit-communityn r/PowerShell
som innehåller hundratals .NET-undantag för att komplettera det här inlägget.
Jag börjar med att söka i listan efter undantag som känns som om de skulle passa bra för min situation. Du bör försöka använda undantag i basnamnområdet System
.
Undantag är objekt
Om du börjar använda många inskrivna undantag bör du komma ihåg att de är objekt. Olika undantag har olika konstruktorer och egenskaper. Om vi tittar på dokumentationen för FileNotFoundException för System.IO.FileNotFoundException
ser vi att vi kan skicka ett meddelande och en filsökväg.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
Och den har en FileName
egenskap som exponerar den filsökvägen.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
Du bör läsa .NET-dokumentationen för andra konstruktorer och objektegenskaper.
Återskapa ett undantag
Om allt du ska göra i ditt catch
block är throw
samma undantag, så gör det inte catch
det. Du bör bara catch
ett undantag som du planerar att hantera eller utföra någon åtgärd när det händer.
Det finns tillfällen då du vill utföra en åtgärd på ett undantag, men sedan generera undantaget igen så att något nedströms kan hantera det. Vi kan skriva ett meddelande eller logga problemet nära där vi upptäcker det, men hantera problemet längre upp i stacken.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Intressant nog kan vi anropa throw
inifrån catch
och det genererar det aktuella undantaget igen.
catch
{
Write-Log $PSItem.ToString()
throw
}
Vi vill återskapa undantaget för att bevara den ursprungliga körningsinformationen som källskript och radnummer. Om vi genererar ett nytt undantag i det här läget döljer det var undantaget startade.
Återskapa ett nytt undantag
Om du får ett undantag men vill kasta ett annat bör du kapsla det ursprungliga undantaget i det nya. På så sätt kan någon i stacken komma åt den som $PSItem.Exception.InnerException
.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
Det enda som jag inte gillar med att använda throw
för råa undantag är att felmeddelandet pekar på -instruktionen throw
och anger att raden är där problemet är.
Unable to find the specified file.
At line:31 char:9
+ throw [System.IO.FileNotFoundException]::new()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], FileNotFoundException
+ FullyQualifiedErrorId : Unable to find the specified file.
Att ha felmeddelandet berätta för mig att mitt skript är brutet eftersom jag anropade throw
på rad 31 är ett dåligt meddelande för användare av skriptet att se. Det säger dem inget användbart.
Dexter Dhami påpekade att jag kan använda ThrowTerminatingError()
för att rätta till det.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Om vi antar att det ThrowTerminatingError()
anropades i en funktion med namnet Get-Resource
är det här felet som vi skulle se.
Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+ Get-Resource -Path $Path
+ ~~~~~~~~~~~~
+ CategoryInfo : OpenError: (:) [Get-Resource], FileNotFoundException
+ FullyQualifiedErrorId : My.ID,Get-Resource
Ser du hur den pekar på Get-Resource
funktionen som källan till problemet? Det talar om för användaren något användbart.
Eftersom $PSItem
är en ErrorRecord
kan vi också använda ThrowTerminatingError
det här sättet för att kasta igen.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Detta ändrar källan till felet till cmdleten och döljer den interna funktionen från användarna av cmdleten.
Försök kan skapa avslutande fel
Kirk Munro påpekar att vissa undantag bara avslutar fel när de körs i ett try/catch
block. Här är exemplet som han gav mig som genererar en uppdelning med noll körningsundantag.
function Start-Something { 1/(1-1) }
Anropa det sedan så här för att se att det genererar felet och fortfarande matar ut meddelandet.
&{ Start-Something; Write-Output "We did it. Send Email" }
Men genom att placera samma kod i en try/catch
ser vi något annat hända.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
Vi ser felet bli ett avslutande fel och inte mata ut det första meddelandet. Vad jag inte gillar med den här är att du kan ha den här koden i en funktion och den fungerar annorlunda om någon använder en try/catch
.
Jag har inte stött på problem med detta själv, men det är hörnfall att vara medveten om.
$PSCmdlet.ThrowTerminatingError() inuti try/catch
En nyans av $PSCmdlet.ThrowTerminatingError()
är att det skapar ett avslutande fel i cmdleten, men det blir ett icke-avslutande fel när den lämnar cmdleten. Detta lämnar ansvaret på anroparen för din funktion att bestämma hur felet ska hanteras. De kan återställa det till ett avslutande fel genom att använda -ErrorAction Stop
eller anropa det inifrån en try{...}catch{...}
.
Offentliga funktionsmallar
En sista ta ett sätt jag hade med mitt samtal med Kirk Munro var att han placerar en try{...}catch{...}
runt varje begin
, process
och end
blockera i alla hans avancerade funktioner. I dessa generiska fångstblock har han en enda rad som använder $PSCmdlet.ThrowTerminatingError($PSItem)
för att hantera alla undantag som lämnar hans funktioner.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Eftersom allt finns i ett try
uttalande inom hans funktioner, agerar allt konsekvent. Detta ger också rena fel till slutanvändaren som döljer den interna koden från det genererade felet.
Fälla
Jag fokuserade på aspekten try/catch
av undantag. Men det finns en äldre funktion som jag måste nämna innan vi avslutar det här.
A trap
placeras i ett skript eller en funktion för att fånga upp alla undantag som inträffar i det omfånget. När ett undantag inträffar körs koden i trap
och sedan fortsätter den normala koden. Om flera undantag inträffar anropas trapen om och om.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
Personligen har jag aldrig antagit den här metoden men jag kan se värdet i administratörs- eller kontrollantskript som loggar alla undantag och sedan fortfarande fortsätter att köras.
Slutkommentarer
Att lägga till korrekt undantagshantering i skripten gör dem inte bara stabilare, utan gör det också enklare för dig att felsöka dessa undantag.
Jag tillbringade mycket tid med att prata throw
eftersom det är ett kärnkoncept när jag pratar om undantagshantering. PowerShell gav oss Write-Error
också som hanterar alla situationer där du skulle använda throw
. Så tro inte att du behöver använda throw
när du har läst detta.
Nu när jag har tagit mig tid att skriva om undantagshantering i den här detaljen ska jag växla över till att använda Write-Error -Stop
för att generera fel i min kod. Jag ska också ta Kirks råd och göra ThrowTerminatingError
min goto undantagshanterare för varje funktion.