Alles wat u wilde weten over uitzonderingen

Foutafhandeling maakt slechts deel uit van het leven als het gaat om het schrijven van code. We kunnen vaak voorwaarden controleren en valideren voor verwacht gedrag. Wanneer de onverwachte gebeurtenis zich voordoet, wordt de afhandeling van uitzonderingen ingeschakeld. U kunt eenvoudig uitzonderingen verwerken die zijn gegenereerd door de code van anderen of u kunt uw eigen uitzonderingen genereren die anderen kunnen verwerken.

Notitie

De oorspronkelijke versie van dit artikel verscheen op het blog geschreven door @KevinMarquette. Het PowerShell-team bedankt Kevin voor het delen van deze inhoud met ons. Bekijk zijn blog op PowerShellExplained.com.

Basisterminologie

We moeten enkele basistermen behandelen voordat we in deze gaan.

Uitzondering

Een uitzondering lijkt op een gebeurtenis die wordt gemaakt wanneer de normale foutafhandeling het probleem niet kan oplossen. Het delen van een getal door nul of het onvoldoende geheugen zijn voorbeelden van iets dat een uitzondering veroorzaakt. Soms maakt de auteur van de code die u gebruikt uitzonderingen voor bepaalde problemen wanneer deze optreden.

Gooien en vangen

Wanneer er een uitzondering optreedt, zeggen we dat er een uitzondering wordt gegenereerd. Als u een gegenereerde uitzondering wilt afhandelen, moet u deze vangen. Als er een uitzondering wordt gegenereerd en deze niet wordt gedetecteerd door iets, stopt het uitvoeren van het script.

De aanroepstack

De aanroepstack is de lijst met functies die elkaar hebben aangeroepen. Wanneer een functie wordt aangeroepen, wordt deze toegevoegd aan de stack of de bovenkant van de lijst. Wanneer de functie wordt afgesloten of geretourneerd, wordt deze verwijderd uit de stack.

Wanneer er een uitzondering wordt gegenereerd, wordt die aanroepstack gecontroleerd zodat een uitzonderingshandler deze kan vangen.

Afsluit- en niet-afsluitfouten

Een uitzondering is over het algemeen een afsluitfout. Er wordt een opgetreden uitzondering opgevangen of de huidige uitvoering wordt beëindigd. Standaard wordt er een niet-afsluitfout gegenereerd Write-Error en wordt er een fout aan de uitvoerstroom toegevoegd zonder een uitzondering te genereren.

Ik wijs hier op omdat Write-Error en andere niet-afsluitfouten de catch.

Een uitzondering inslikken

Dit is wanneer u een fout ondervangt om deze te onderdrukken. Doe dit met voorzichtigheid, omdat het het oplossen van problemen erg moeilijk kan maken.

Syntaxis van de basisopdracht

Hier volgt een kort overzicht van de basissyntaxis voor het verwerken van uitzonderingen die worden gebruikt in PowerShell.

Werpen

Om onze eigen uitzonderingsevenement te maken, genereren we een uitzondering met het throw trefwoord.

function Start-Something
{
    throw "Bad thing happened"
}

Hiermee maakt u een runtime-uitzondering die een afsluitfout is. Het wordt verwerkt door een catch aanroepende functie of sluit het script af met een bericht zoals dit.

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

Ik heb gezegd dat Write-Error er standaard geen afsluitfout wordt gegenereerd. Als u opgeeft -ErrorAction Stop, Write-Error genereert u een afsluitfout die kan worden verwerkt met een catch.

Write-Error -Message "Houston, we have a problem." -ErrorAction Stop

Dank u aan Lee Dailey om u eraan te herinneren dat u deze manier wilt gebruiken -ErrorAction Stop .

Cmdlet -ErrorAction Stop

Als u opgeeft -ErrorAction Stop voor een geavanceerde functie of cmdlet, worden alle Write-Error instructies omgezet in afsluitfouten die de uitvoering stoppen of die kunnen worden verwerkt door een catch.

Start-Something -ErrorAction Stop

Try/Catch

De manier waarop uitzonderingsafhandeling werkt in PowerShell (en vele andere talen) is dat u eerst try een codesectie maakt en als er een fout optreedt, kunt catch u dit doen. Hier volgt een snel voorbeeld.

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 $_
}

Het catch script wordt alleen uitgevoerd als er een afsluitfout optreedt. Als de try uitvoering correct wordt uitgevoerd, wordt het overgeslagen op de catch. U hebt toegang tot de uitzonderingsgegevens in het catch blok met behulp van de $_ variabele.

Probeer het ten slotte

Soms hoeft u geen fout af te handelen, maar nog steeds code nodig om uit te voeren als er een uitzondering optreedt of niet. Een finally script doet dat precies.

Bekijk dit voorbeeld:

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()

Wanneer u een resource opent of er verbinding mee maakt, moet u deze sluiten. Als er ExecuteNonQuery() een uitzondering wordt gegenereerd, wordt de verbinding niet gesloten. Hier ziet u dezelfde code in een try/finally blok.

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
    $command.Connection.Open()
    $command.ExecuteNonQuery()
}
finally
{
    $command.Connection.Close()
}

In dit voorbeeld wordt de verbinding gesloten als er een fout optreedt. Het is ook gesloten als er geen fout is. Het finally script wordt elke keer uitgevoerd.

Omdat u de uitzondering niet ondervangt, wordt deze nog steeds doorgegeven aan de aanroepstack.

Try/Catch/Finally

Het is perfect geldig om te gebruiken catch en finally samen. Meestal gebruikt u de ene of de andere, maar mogelijk vindt u scenario's waarin u beide gebruikt.

$PSItem

Nu we de basisbeginselen uit de weg hebben, kunnen we wat dieper graven.

In het catch blok bevindt zich een automatische variabele ($PSItem of $_) van het type ErrorRecord dat de details over de uitzondering bevat. Hier volgt een kort overzicht van enkele van de belangrijkste eigenschappen.

Voor deze voorbeelden heb ik een ongeldig pad ReadAllText gebruikt om deze uitzondering te genereren.

[System.IO.File]::ReadAllText( '\\test\no\filefound.log')

PSItem.ToString()

Dit geeft u het schoonste bericht dat u kunt gebruiken in logboekregistratie en algemene uitvoer. ToString() wordt automatisch aangeroepen als $PSItem deze in een tekenreeks wordt geplaatst.

catch
{
    Write-Output "Ran into an issue: $($PSItem.ToString())"
}

catch
{
    Write-Output "Ran into an issue: $PSItem"
}

$PSItem.InvocationInfo

Deze eigenschap bevat aanvullende informatie die door PowerShell wordt verzameld over de functie of het script waarin de uitzondering is opgetreden. Hier volgt de InvocationInfo voorbeelduitzondering die ik heb gemaakt.

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

De belangrijke details hier tonen de ScriptName, de Line code en de ScriptLineNumber plaats waar de aanroep is gestart.

$PSItem.ScriptStackTrace

Deze eigenschap toont de volgorde van functie-aanroepen waarmee u naar de code bent gekomen waarin de uitzondering is gegenereerd.

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

Ik maak alleen aanroepen naar functies in hetzelfde script, maar hiermee worden de aanroepen bijgehouden als er meerdere scripts betrokken waren.

$PSItem.Exception

Dit is de werkelijke uitzondering die is gegenereerd.

$PSItem.Exception.Message

Dit is het algemene bericht dat de uitzondering beschrijft en een goed uitgangspunt is bij het oplossen van problemen. De meeste uitzonderingen hebben een standaardbericht, maar kunnen ook worden ingesteld op iets aangepast wanneer de uitzondering wordt gegenereerd.

PS> $PSItem.Exception.Message

Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."

Dit is ook het bericht dat wordt geretourneerd bij het bellen $PSItem.ToString() als er niet één is ingesteld op de ErrorRecord.

$PSItem.Exception.InnerException

Uitzonderingen kunnen interne uitzonderingen bevatten. Dit is vaak het geval wanneer de code die u aanroept een uitzondering onderschept en een andere uitzondering genereert. De oorspronkelijke uitzondering wordt in de nieuwe uitzondering geplaatst.

PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

Ik zal dit later opnieuw bekijken wanneer ik het heb over het opnieuw genereren van uitzonderingen.

$PSItem.Exception.StackTrace

Dit is de StackTrace uitzondering. Ik heb een ScriptStackTrace bovenstaande laten zien, maar deze is bedoeld voor de aanroepen naar beheerde code.

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 )

U krijgt deze stack-trace alleen wanneer de gebeurtenis wordt gegenereerd vanuit beheerde code. Ik roep rechtstreeks een .NET Framework-functie aan, zodat dit alles is wat we in dit voorbeeld kunnen zien. Over het algemeen zoekt u wanneer u een stack-trace bekijkt, naar waar uw code stopt en de systeemoproepen beginnen.

Werken met uitzonderingen

Er zijn meer uitzonderingen dan de basissyntaxis en uitzonderingseigenschappen.

Getypte uitzonderingen vangen

U kunt selectief zijn met de uitzonderingen die u ondervangt. Uitzonderingen hebben een type en u kunt het type uitzondering opgeven dat u wilt vangen.

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

Het uitzonderingstype wordt voor elk catch blok gecontroleerd totdat er een wordt gevonden die overeenkomt met uw uitzondering. Het is belangrijk om te beseffen dat uitzonderingen kunnen overnemen van andere uitzonderingen. In het bovenstaande FileNotFoundException voorbeeld neemt u het over van IOException. Dus als de IOException eerste was, zou het in plaats daarvan worden gebeld. Er wordt slechts één catch-blok aangeroepen, zelfs als er meerdere overeenkomsten zijn.

Als we een System.IO.PathTooLongExceptionhadden, zou de IOException match, maar als we een InsufficientMemoryException hadden, zou niets het vangen en zou het de stapel doorgeven.

Meerdere typen tegelijk vangen

Het is mogelijk om meerdere uitzonderingstypen met dezelfde catch instructie te vangen.

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]"
}

/u/Sheppard_Ra Bedankt voor het voorstellen van deze toevoeging.

Getypte uitzonderingen genereren

U kunt getypte uitzonderingen genereren in PowerShell. In plaats van aan te roepen throw met een tekenreeks:

throw "Could not find: $path"

Gebruik een uitzonderingsversneller als volgt:

throw [System.IO.FileNotFoundException] "Could not find: $path"

Maar u moet een bericht opgeven wanneer u het op die manier doet.

U kunt ook een nieuwe instantie van een uitzondering maken die moet worden gegenereerd. Het bericht is optioneel wanneer u dit doet omdat het systeem standaardberichten bevat voor alle ingebouwde uitzonderingen.

throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")

Als u PowerShell 5.0 of hoger niet gebruikt, moet u de oudere New-Object benadering gebruiken.

throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")

Met behulp van een getypte uitzondering kunt u (of anderen) de uitzondering vangen door het type zoals vermeld in de vorige sectie.

Write-Error -Uitzondering

We kunnen deze getypte uitzonderingen Write-Error toevoegen en we kunnen de fouten nog steeds op catch uitzonderingstype toepassen. Gebruik Write-Error zoals in deze voorbeelden:

# 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

Dan kunnen we het als volgt vangen:

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

De grote lijst met .NET-uitzonderingen

Ik heb een hoofdlijst gecompileerd met behulp van de Reddit/r/PowerShell-community die honderden .NET-uitzonderingen bevat om dit bericht aan te vullen.

Ik begin met het zoeken naar die lijst naar uitzonderingen die het gevoel hebben dat ze geschikt zijn voor mijn situatie. Probeer uitzonderingen te gebruiken in de basisnaamruimte System .

Uitzonderingen zijn objecten

Als u veel getypte uitzonderingen gaat gebruiken, moet u er rekening mee houden dat het objecten zijn. Verschillende uitzonderingen hebben verschillende constructors en eigenschappen. Als we de FileNotFoundException-documentatie bekijken, System.IO.FileNotFoundExceptionzien we dat we een bericht en een bestandspad kunnen doorgeven.

[System.IO.FileNotFoundException]::new("Could not find file", $path)

En het heeft een FileName eigenschap die dat bestandspad beschikbaar maakt.

catch [System.IO.FileNotFoundException]
{
    Write-Output $PSItem.Exception.FileName
}

Raadpleeg de .NET-documentatie voor andere constructors en objecteigenschappen.

Een uitzondering opnieuw genereren

Als alles wat u in uw catch blok gaat doen, dezelfde uitzondering is throw , doet u dat niet catch . U moet alleen catch een uitzondering afhandelen of een actie uitvoeren wanneer dit gebeurt.

Er zijn momenten waarop u een actie wilt uitvoeren op een uitzondering, maar de uitzondering opnieuw wilt genereren, zodat er iets downstream mee kan omgaan. We kunnen een bericht schrijven of het probleem registreren dicht bij de locatie waar we het detecteren, maar het probleem verder op de stapel afhandelen.

catch
{
    Write-Log $PSItem.ToString()
    throw $PSItem
}

Interessant genoeg kunnen we vanuit binnen catch bellen throw en de huidige uitzondering opnieuw genereren.

catch
{
    Write-Log $PSItem.ToString()
    throw
}

We willen de uitzondering opnieuw genereren om de oorspronkelijke uitvoeringsinformatie, zoals bronscript en regelnummer, te behouden. Als er op dit moment een nieuwe uitzondering wordt gegenereerd, wordt verborgen waar de uitzondering is gestart.

Een nieuwe uitzondering opnieuw genereren

Als u een uitzondering ondervangt, maar u een andere wilt gooien, moet u de oorspronkelijke uitzondering in de nieuwe nesten. Hierdoor kan iemand de stack openen als de $PSItem.Exception.InnerException.

catch
{
    throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}

$PSCmdlet.ThrowTerminatingError()

Het enige wat ik niet leuk vind om te gebruiken throw voor onbewerkte uitzonderingen, is dat het foutbericht verwijst naar de throw instructie en aangeeft dat de regel is waar het probleem zich bevindt.

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.

Als het foutbericht mij laat weten dat mijn script is verbroken omdat ik throw op regel 31 belde, is een slecht bericht voor gebruikers van uw script om te zien. Het vertelt ze niets nuttigs.

Dexter Dhami wees erop dat ik dat kan gebruiken ThrowTerminatingError() om dat te corrigeren.

$PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
        ([System.IO.FileNotFoundException]"Could not find $Path"),
        'My.ID',
        [System.Management.Automation.ErrorCategory]::OpenError,
        $MyObject
    )
)

Als we ervan uitgaan dat deze ThrowTerminatingError() is aangeroepen binnen een functie die wordt aangeroepen Get-Resource, is dit de fout die we zouden zien.

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

Ziet u hoe deze naar de Get-Resource functie verwijst als de bron van het probleem? Dat vertelt de gebruiker iets nuttigs.

Omdat $PSItem is een ErrorRecord, kunnen we ook deze manier gebruiken ThrowTerminatingError om opnieuw te gooien.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

Hiermee wijzigt u de bron van de fout in de cmdlet en verbergt u de interne functies van uw functie van de gebruikers van uw cmdlet.

Probeer afsluitfouten te maken

Kirk Munro wijst erop dat sommige uitzonderingen alleen afsluitfouten zijn wanneer ze in een try/catch blok worden uitgevoerd. Hier volgt het voorbeeld dat hij me gaf dat een deel genereert door nul runtime-uitzondering.

function Start-Something { 1/(1-1) }

Roep het als volgt aan om de fout te genereren en het bericht nog steeds uit te voeren.

&{ Start-Something; Write-Output "We did it. Send Email" }

Maar door diezelfde code in een try/catchte plaatsen, zien we iets anders gebeuren.

try
{
    &{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
    Write-Output "Notify Admin to fix error and send email"
}

We zien dat de fout een afsluitfout wordt en het eerste bericht niet wordt uitgevoerd. Wat ik niet leuk vind aan deze code is dat je deze code in een functie kunt hebben en dat het anders werkt als iemand een try/catch.

Ik heb hier zelf geen problemen mee te maken, maar het is hoekgeval om op de hoogte te zijn.

$PSCmdlet.ThrowTerminatingError() in try/catch

Een nuance hiervan $PSCmdlet.ThrowTerminatingError() is dat er een afsluitfout in uw cmdlet wordt gemaakt, maar het verandert in een niet-afsluitfout nadat de cmdlet is verlaten. Dit laat de belasting van de aanroeper van uw functie staan om te bepalen hoe de fout moet worden afgehandeld. Ze kunnen deze weer omzetten in een afsluitfout door het te gebruiken -ErrorAction Stop of aan te roepen vanuit een try{...}catch{...}.

Sjablonen voor openbare functies

Een laatste manier die ik had met mijn gesprek met Kirk Munro was dat hij een try{...}catch{...} rond elke begin, process en end blok in al zijn geavanceerde functies plaatst. In die generieke catch blocks heeft hij één regel die gebruikt $PSCmdlet.ThrowTerminatingError($PSItem) om alle uitzonderingen af te handelen die zijn functies verlaten.

function Start-Something
{
    [CmdletBinding()]
    param()

    process
    {
        try
        {
            ...
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

Omdat alles in een try verklaring binnen zijn functies staat, werkt alles consistent. Dit geeft ook schone fouten aan de eindgebruiker die de interne code van de gegenereerde fout verbergt.

Val

Ik heb me gericht op het try/catch aspect van uitzonderingen. Maar er is één verouderde functie die ik moet vermelden voordat we dit inpakken.

Een trap wordt in een script of functie geplaatst om alle uitzonderingen te vangen die in dat bereik plaatsvinden. Wanneer er een uitzondering optreedt, wordt de code in de trap code uitgevoerd en wordt de normale code voortgezet. Als er meerdere uitzonderingen optreden, wordt de trap over en weer aangeroepen.

trap
{
    Write-Log $PSItem.ToString()
}

throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')

Ik heb deze benadering persoonlijk nooit aangenomen, maar ik kan de waarde zien in beheerders- of controllerscripts die alle uitzonderingen registreren en vervolgens nog steeds worden uitgevoerd.

Opmerkingen sluiten

Het toevoegen van de juiste verwerking van uitzonderingen aan uw scripts maakt ze niet alleen stabieler, maar maakt het ook gemakkelijker voor u om problemen met deze uitzonderingen op te lossen.

Ik heb veel tijd besteed aan praten throw omdat het een kernconcept is bij het behandelen van uitzonderingen. PowerShell gaf ons Write-Error ook die alle situaties verwerkt waarin u zou gebruiken throw. Denk dus niet dat u dit moet gebruiken throw nadat u dit hebt gelezen.

Nu ik de tijd heb genomen om over het verwerken van uitzonderingen in dit detail te schrijven, ga ik overschakelen naar het gebruik Write-Error -Stop om fouten in mijn code te genereren. Ik neem ook Kirk's advies en maak ThrowTerminatingError mijn goto uitzonderingshandler voor elke functie.