Dela via


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 StopWrite-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 ScriptNamekodens , 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.FileNotFoundExceptionser 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 ErrorRecordkan 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/catchser 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.