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.
Werpen
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]"
}
Bedankt Redditor u/Sheppard_Ra
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-community r/PowerShell
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/catch
te 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 begin
process
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.