Share via


Alles wat u wilde weten over uitzonderingen

Foutafhandeling is slechts een onderdeel van het leven als het gaat om het schrijven van code. We kunnen vaak voorwaarden voor verwacht gedrag controleren en valideren. Wanneer de onverwachte gebeurtenis zich voordoet, schakelen we over naar de afhandeling van uitzonderingen. 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 dat is 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 bespreken voordat we in deze gaan.

Uitzondering

Een uitzondering lijkt op een gebeurtenis die wordt gemaakt wanneer normale foutafhandeling het probleem niet kan oplossen. Een getal delen door nul of onvoldoende geheugen zijn voorbeelden van iets dat een uitzondering creëert. 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 onderschept 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. Een gegenereerde uitzondering wordt gevangen of de huidige uitvoering wordt beëindigd. Standaard wordt een niet-afsluitfout gegenereerd Write-Error en wordt er een fout aan de uitvoerstroom toegevoegd zonder dat er een uitzondering wordt gegenereerd.

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

Een uitzondering inslikken

Dit is wanneer u een fout onderscheppen om deze te onderdrukken. Doe dit met voorzichtigheid, omdat het het oplossen van problemen heel 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.

Gooien

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

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

Hiermee wordt een runtime-uitzondering gemaakt 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 voor het herinneren aan het gebruik -ErrorAction Stop van deze manier.

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

Zie about_CommonParameters voor meer informatie over de parameter ErrorAction. Zie about_Preference_Variables voor meer informatie over de $ErrorActionPreference variabele.

Try/Catch

De manier waarop uitzonderingsafhandeling in PowerShell (en vele andere talen) werkt, 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 kunt toegang krijgen tot de uitzonderingsgegevens in het catch blok met behulp van de $_ variabele.

Probeer/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 een of meer, maar u kunt scenario's vinden 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 zijn.

$PSItem.Exception

Dit is de werkelijke uitzondering die is opgetreden.

$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 bezoeken wanneer ik het heb over het opnieuw genereren van uitzonderingen.

$PSItem.Exception.StackTrace

Dit is de StackTrace uitzondering. Ik heb een ScriptStackTrace bovenstaande getoond, maar dit is 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 een .NET Framework-functie rechtstreeks aan, dus dat is alles wat we in dit voorbeeld kunnen zien. Over het algemeen zoekt u wanneer u een stacktracering 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 onderscheppen. 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 wordt overgenomen 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.PathTooLongException, dan zou de IOException match, maar als we een InsufficientMemoryException dan niets zouden vangen zou het en het zou 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 dit op die manier doet.

U kunt ook een nieuw exemplaar van een uitzondering maken die moet worden gegenereerd. Het bericht is optioneel wanneer u dit doet omdat het systeem standaardberichten heeft 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 methode 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 ondervangen door het type zoals vermeld in de vorige sectie.

Write-Error -Exception

We kunnen deze getypte uitzonderingen Write-Error toevoegen aan en we kunnen de fouten nog steeds op catch uitzonderingstype toepassen. Gebruik Write-Error dit als 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. U moet uitzonderingen in de basisnaamruimte System gebruiken.

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.FileNotFoundException, zien 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 , doe het dan 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 genereert, zodat er iets downstream mee kan omgaan. We kunnen een bericht schrijven of het probleem in de buurt vastleggen van waar we het detecteren, maar het probleem verderop in de stapel afhandelen.

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

Interessant genoeg kunnen we vanuit binnen catch de aanroepen 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 we op dit moment een nieuwe uitzondering genereren, wordt verborgen waar de uitzondering is gestart.

Een nieuwe uitzondering opnieuw genereren

Als u een uitzondering onderscheppen, maar u een andere wilt genereren, moet u de oorspronkelijke uitzondering in de nieuwe nesten. Hierdoor kan iemand in de stapel toegang krijgen tot de stack 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 wordt weergegeven, wordt mij verteld dat mijn script is verbroken omdat ik op regel 31 een slecht bericht heb aangeroepen throw voor gebruikers van uw script om dit 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 die 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 verwijst naar de Get-Resource functie 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 voor 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 waarmee een deel wordt gegenereerd door een runtime-uitzondering zonder runtime.

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

Roep deze 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 dat er iets anders gebeurt.

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

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

Ik heb hier zelf geen problemen mee gehad, maar het is een hoekzaak waar ik rekening mee moet houden.

$PSCmdlet.ThrowTerminatingError() in try/catch

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

Sjablonen voor openbare functies

Een laatste manier om een manier te nemen die ik met Kirk Munro had, was dat hij overal een try{...}catch{...} rondje beginprocess plaatst en end blokkeert in al zijn geavanceerde functies. In die algemene catch blocks heeft hij één regel $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 richt me 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 ondervangen 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 steeds opnieuw 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 nooit gebruikt, maar ik kan de waarde zien in admin- of controllerscripts die alle uitzonderingen registreren en vervolgens nog steeds worden uitgevoerd.

Afsluitende opmerkingen

Het toevoegen van de juiste verwerking van uitzonderingen aan uw scripts maakt ze niet alleen stabieler, maar maakt het ook eenvoudiger 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 dat alle situaties afhandelt 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 te schrijven over het verwerken van uitzonderingen in dit detail, 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 exception handler voor elke functie.