Tutto ciò che volevi sapere sulle eccezioni
La gestione degli errori è solo parte della vita quando si tratta di scrivere codice. Spesso è possibile controllare e convalidare le condizioni per il comportamento previsto. Quando si verifica un evento imprevisto, si esegue la gestione delle eccezioni. È possibile gestire facilmente le eccezioni generate dal codice di altri utenti oppure generare eccezioni personalizzate per gli altri utenti da gestire.
Nota
La versione originale di questo articolo è apparsa nel blog scritto da @KevinMarquette. Il team di PowerShell ringrazia Kevin per aver condiviso questo contenuto con noi. Consultare il suo blog all'indirizzo PowerShellExplained.com.
Terminologia di base
Dobbiamo coprire alcuni termini di base prima di passare a questo.
Eccezione
Un'eccezione è simile a un evento creato quando la gestione degli errori normale non può gestire il problema. Il tentativo di dividere un numero per zero o di esaurimento della memoria sono esempi di elementi che creano un'eccezione. A volte l'autore del codice in uso crea eccezioni per determinati problemi quando si verificano.
Lanciare ed intercettare
Quando si verifica un'eccezione, si dice che viene lanciata un'eccezione. Per gestire un'eccezione lanciata, è necessario intercettarla. Se viene generata un'eccezione e non viene intercettata da un elemento, lo script interrompe l'esecuzione.
Stack di chiamate
Lo stack di chiamate è l'elenco di funzioni che hanno chiamato l'una l'altra. Quando viene chiamata una funzione, viene aggiunta allo stack o all'inizio dell'elenco. Quando la funzione esce o ritorna, viene rimossa dallo stack.
Quando viene generata un'eccezione, lo stack di chiamate viene controllato affinché un gestore di eccezioni lo intercetti.
Errori di terminazione e non di terminazione
Un'eccezione è in genere un errore fatale. Un'eccezione generata viene intercettata o termina l'esecuzione corrente. Per impostazione predefinita, un errore non irreversibile viene generato da Write-Error
e aggiunge un errore al flusso di output senza generare un'eccezione.
Sottolineo questo perché Write-Error
e altri errori non terminanti non attivano il catch
.
Ingoiando un'eccezione
Questo è quando si rileva un errore solo per eliminarlo. Eseguire questa operazione con cautela perché può rendere molto difficile la risoluzione dei problemi.
Sintassi dei comandi di base
Ecco una rapida panoramica della sintassi di gestione delle eccezioni di base usata in PowerShell.
Lancio
Per creare un evento di eccezione personalizzato, viene generata un'eccezione con la parola chiave throw
.
function Start-Something
{
throw "Bad thing happened"
}
In questo modo viene creata un'eccezione di runtime che è un errore di terminazione. Viene gestito da un catch
in una funzione chiamante o esce dallo script con un messaggio simile al seguente.
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
Ho detto che Write-Error
non genera un errore irreversibile per impostazione predefinita. Se si specifica -ErrorAction Stop
, Write-Error
genera un errore di termine che può essere gestito con un catch
.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
Grazie a Lee Dailey per aver ricordato l'uso di -ErrorAction Stop
in questo modo.
Cmdlet -ErrorAction Stop
Se si specifica -ErrorAction Stop
in qualsiasi funzione o cmdlet avanzato, tutte le istruzioni Write-Error
vengono trasformate in errori di cessazione che interrompono l'esecuzione o che possono essere gestiti da un catch
.
Start-Something -ErrorAction Stop
Per altre informazioni sul parametro ErrorAction, vedere about_CommonParameters. Per altre informazioni sulla variabile $ErrorActionPreference
, vedere about_Preference_Variables.
Prova/Cattura
Il modo in cui la gestione delle eccezioni funziona in PowerShell (e molti altri linguaggi) è che si try
prima di tutto una sezione di codice e, se genera un errore, è possibile catch
. Ecco un rapido esempio.
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 $_
}
Lo script catch
viene eseguito solo in caso di un errore irreversibile. Se il try
viene eseguito correttamente, ignora il catch
. È possibile accedere alle informazioni sull'eccezione nel blocco catch
usando la variabile $_
.
Prova/Infine
In alcuni casi non è necessario gestire un errore, ma è comunque necessario codice da eseguire se si verifica o meno un'eccezione. Uno script di finally
esegue esattamente questa operazione.
Esaminare questo esempio:
$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
Ogni volta che si apre o ci si connette a una risorsa, è necessario chiuderla. Se il ExecuteNonQuery()
genera un'eccezione, la connessione non viene chiusa. Ecco lo stesso codice all'interno di un blocco try/finally
.
$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
In questo esempio la connessione viene chiusa se si verifica un errore. Viene anche chiuso se non è presente alcun errore. Lo script finally
viene eseguito ogni volta.
Poiché l'eccezione non viene rilevata, viene comunque propagata nella pila delle chiamate.
Prova/Cattura/Finalmente
È perfettamente valido usare catch
e finally
insieme. Nella maggior parte dei casi si userà uno o l'altro, ma è possibile trovare scenari in cui si usano entrambi.
$PSItem
Ora che abbiamo ottenuto le nozioni di base, possiamo scavare un po 'più profondo.
All'interno del blocco catch
è presente una variabile automatica ($PSItem
o $_
) di tipo ErrorRecord
che contiene i dettagli sull'eccezione. Ecco una rapida panoramica di alcune delle proprietà chiave.
Per questi esempi è stato usato un percorso non valido in ReadAllText
per generare questa eccezione.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
In questo modo ottieni il messaggio più pulito da usare nei log e nell'output generale.
ToString()
viene chiamato automaticamente se $PSItem
viene inserito all'interno di una stringa.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
Questa proprietà contiene informazioni aggiuntive raccolte da PowerShell sulla funzione o sullo script in cui è stata generata l'eccezione. Di seguito è riportato il InvocationInfo
dell'eccezione di esempio creata.
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
I dettagli importanti qui mostrano il ScriptName
, il Line
di codice e il ScriptLineNumber
in cui è stata avviata la chiamata.
$PSItem.ScriptStackTrace
Questa proprietà mostra l'ordine delle chiamate di funzione che hanno portato al codice in cui è stata generata l'eccezione.
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
Sto solo effettuando chiamate alle funzioni nello stesso script, ma questo tiene traccia delle chiamate se fossero coinvolti più script.
$PSItem.Exception
Si tratta dell'eccezione effettiva generata.
$PSItem.Exception.Message
Si tratta del messaggio generale che descrive l'eccezione ed è un buon punto di partenza per la risoluzione dei problemi. La maggior parte delle eccezioni ha un messaggio predefinito, ma può anche essere impostata su un elemento personalizzato quando viene generata l'eccezione.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Questo è anche il messaggio restituito quando si chiama $PSItem.ToString()
se non ne è stato impostato uno nel ErrorRecord
.
$PSItem.Exception.InnerException
Le eccezioni possono contenere eccezioni interne. Questo è spesso il caso in cui il codice che si chiama intercetta un'eccezione e genera un'eccezione diversa. L'eccezione originale viene inserita all'interno della nuova eccezione.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
Lo rivisiterò più tardi quando parlo di generare nuovamente eccezioni.
$PSItem.Exception.StackTrace
Si tratta del StackTrace
per l'eccezione. Ho mostrato una ScriptStackTrace
prima, ma questa è per le chiamate al codice gestito.
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 )
Questa traccia dello stack viene ottenuta solo quando l'evento viene generato dal codice gestito. Sto chiamando direttamente una funzione del .NET Framework, quindi è tutto ciò che possiamo vedere in questo esempio. In genere, quando si esamina un'analisi dello stack, si sta cercando dove si arresta il codice e iniziano le chiamate di sistema.
Uso delle eccezioni
Esistono più eccezioni rispetto alla sintassi di base e alle proprietà delle eccezioni.
Rilevamento di eccezioni tipizzate
È possibile essere selettivi con le eccezioni rilevate. Le eccezioni hanno un tipo ed è possibile specificare il tipo di eccezione che si vuole intercettare.
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"
}
Il tipo di eccezione viene controllato per ogni blocco catch
fino a quando non viene trovata una corrispondenza con l'eccezione.
È importante tenere presente che le eccezioni possono ereditare da altre eccezioni. Nell'esempio precedente, FileNotFoundException
eredita da IOException
. Quindi, se il IOException
fosse prima, allora verrebbe chiamato invece. Anche se sono presenti più corrispondenze, viene richiamato un solo blocco catch.
Se avessimo un System.IO.PathTooLongException
, l'IOException
sarebbe conforme, ma se avessimo un InsufficientMemoryException
, allora nulla lo intercetterebbe e propagherebbe lungo lo stack.
Intercettare più tipi contemporaneamente
È possibile intercettare più tipi di eccezione con la stessa istruzione catch
.
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]"
}
Grazie redditor u/Sheppard_Ra
per aver suggerito questa aggiunta.
Lancio di eccezioni tipizzate
È possibile generare eccezioni tipate in PowerShell. Anziché chiamare throw
con una stringa:
throw "Could not find: $path"
Usare un acceleratore di eccezioni simile al seguente:
throw [System.IO.FileNotFoundException] "Could not find: $path"
È tuttavia necessario specificare un messaggio quando lo si esegue in questo modo.
È anche possibile creare una nuova istanza di un'eccezione da lanciare. Il messaggio è facoltativo quando si esegue questa operazione perché il sistema dispone di messaggi predefiniti per tutte le eccezioni predefinite.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
Se non si usa PowerShell 5.0 o versione successiva, è necessario usare l'approccio New-Object
precedente.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
Usando un'eccezione tipizzata, l'utente (o altri) può intercettare l'eccezione in base al tipo indicato nella sezione precedente.
Write-Error -Exception
È possibile aggiungere queste eccezioni tipate a Write-Error
ed è comunque possibile catch
gli errori in base al tipo di eccezione. Usare Write-Error
come negli esempi seguenti:
# 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
È quindi possibile intercettarlo come segue:
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
Elenco completo di eccezioni .NET
Ho compilato un elenco master con l'aiuto della community di r/PowerShell
Reddit che contiene centinaia di eccezioni .NET per integrare questo post.
Inizio cercando in quell'elenco eccezioni che sembrano essere una buona soluzione per la mia situazione. È consigliabile provare a usare le eccezioni nello spazio dei nomi di base System
.
Le eccezioni sono oggetti
Se si inizia a usare molte eccezioni tipate, tenere presente che si tratta di oggetti . Eccezioni diverse hanno costruttori e proprietà diversi. Se esaminiamo la documentazione di FileNotFoundException per System.IO.FileNotFoundException
, vediamo che è possibile passare un messaggio e un percorso di file.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
E ha una proprietà FileName
che espone il percorso del file.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
È consigliabile consultare la documentazione di .NET per altri costruttori e proprietà degli oggetti.
Rilancio di un'eccezione
Se tutto ciò che vuoi fare nel blocco di catch
è throw
la stessa eccezione, non catch
. È consigliabile catch
un'eccezione che si prevede di gestire o eseguire alcune azioni quando si verifica.
In alcuni casi è possibile eseguire un'azione su un'eccezione, ma generare nuovamente l'eccezione in modo che un elemento downstream possa gestirlo. È possibile scrivere un messaggio o registrare il problema vicino a dove viene rilevato, ma gestirlo più in alto nello stack.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
È interessante notare che è possibile chiamare throw
dall'interno del catch
e genera nuovamente l'eccezione corrente.
catch
{
Write-Log $PSItem.ToString()
throw
}
Si vuole generare nuovamente l'eccezione per mantenere le informazioni di esecuzione originali, ad esempio lo script di origine e il numero di riga. Se a questo punto lanciamo una nuova eccezione, viene nascosta la posizione in cui l'eccezione è stata lanciata.
Ri-generazione di una nuova eccezione
Se si intercetta un'eccezione ma si vuole generare un'eccezione diversa, è necessario annidare l'eccezione originale all'interno di quella nuova. Ciò consente a qualcuno più in basso nella gerarchia dello stack di accedervi come $PSItem.Exception.InnerException
.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
La sola cosa che non mi piace dell'utilizzare throw
per le eccezioni non elaborate è che il messaggio di errore punta all'istruzione throw
e indica che quella è la linea in cui si verifica il problema.
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.
Se il messaggio di errore mi dice che il mio script è malfunzionante perché ho chiamato throw
alla riga 31, è un messaggio problematico per gli utenti dello script. Non le dice nulla di utile.
Dexter Dhami ha sottolineato che posso usare ThrowTerminatingError()
per correggerlo.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Se si presuppone che ThrowTerminatingError()
sia stato chiamato all'interno di una funzione denominata Get-Resource
, questo è l'errore visualizzato.
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
Vedi come punta alla funzione Get-Resource
come origine del problema? Questo indica all'utente qualcosa di utile.
Poiché $PSItem
è un ErrorRecord
, è anche possibile usare ThrowTerminatingError
in questo modo per rilanciare.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
In questo modo l'origine dell'errore viene modificata nel cmdlet e vengono nascosti gli elementi interni della funzione dagli utenti del cmdlet.
Provare a creare errori di terminazione
Kirk Munro sottolinea che alcune eccezioni sono solo errori irreversibili quando vengono eseguiti all'interno di un blocco try/catch
. Ecco l'esempio che mi ha dato che genera una divisione per un'eccezione di runtime zero.
function Start-Something { 1/(1-1) }
Richiamarlo come questo per visualizzare l'errore e restituire comunque il messaggio.
&{ Start-Something; Write-Output "We did it. Send Email" }
Tuttavia, inserendo lo stesso codice all'interno di un try/catch
, viene visualizzato un altro evento.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
Si noterà che l'errore diventa un errore irreversibile e non restituisce il primo messaggio. Ciò che non mi piace di questo è che è possibile avere questo codice in una funzione e agisce in modo diverso se qualcuno usa un try/catch
.
Non ho affrontato problemi personalmente, ma è un caso limite di cui essere consapevoli.
$PSCmdlet.ThrowTerminatingError() all'interno di try/catch
Una sfumatura di $PSCmdlet.ThrowTerminatingError()
è che crea un errore di terminazione all'interno del Cmdlet, ma si trasforma in un errore non di terminazione dopo che lascia il Cmdlet. In questo modo, spetta al chiamante della tua funzione decidere come gestire l'errore. Possono trasformarlo nuovamente in un errore irreversibile utilizzando -ErrorAction Stop
o chiamandolo all'interno di un try{...}catch{...}
.
Modelli di funzione pubblici
Un ultimo aspetto che ho tratto dalla mia conversazione con Kirk Munro è che posiziona un try{...}catch{...}
intorno a ogni blocco begin
, process
e end
in tutte le sue funzioni avanzate. In questi blocchi catch generici, ha una singola riga che utilizza $PSCmdlet.ThrowTerminatingError($PSItem)
per gestire tutte le eccezioni che escono dalle sue funzioni.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Poiché tutto è in una dichiarazione try
all'interno delle sue funzioni, tutto funziona in modo coerente. In questo modo all'utente finale vengono presentati errori chiari che nascondono il codice interno dall'errore generato.
Trappola
Mi sono concentrato sull'aspetto try/catch
delle eccezioni. Ma c'è una funzionalità legacy che devo menzionare prima di completare questa operazione.
Un trap
viene inserito in uno script o in una funzione per rilevare tutte le eccezioni che si verificano in tale ambito. Quando si verifica un'eccezione, viene eseguito il codice nel trap
e quindi il codice normale continua. Se si verificano più eccezioni, la trap viene chiamata ripetutamente.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
Personalmente non ho mai adottato questo approccio, ma posso vedere il valore negli script di amministrazione o controller che registrano tutte le eccezioni e quindi continuano a essere eseguiti.
Osservazioni di chiusura
L'aggiunta di una gestione corretta delle eccezioni agli script non solo li rende più stabili, ma semplifica anche la risoluzione di tali eccezioni.
Ho dedicato molto tempo a parlare throw
perché si tratta di un concetto fondamentale quando si parla di gestione delle eccezioni. PowerShell ha anche fornito Write-Error
che gestisce tutte le situazioni in cui si userebbe throw
. Non pensare quindi che sia necessario usare throw
dopo la lettura.
Ora che ho preso il tempo di scrivere sulla gestione delle eccezioni in questo dettaglio, passerò all'uso di Write-Error -Stop
per generare errori nel codice. Seguirò anche i consigli di Kirk e imposterò ThrowTerminatingError
come gestore di eccezioni predefinito per ogni funzione.