Condividi tramite


Tutto quello che c'è da sapere sulle eccezioni

Quando si scrive codice, la gestione degli errori fa parte del processo. Spesso è possibile verificare e convalidare il comportamento previsto tramite le condizioni. Quando si verifica qualcosa di imprevisto, entra in gioco la gestione delle eccezioni. È possibile gestire facilmente le eccezioni generate dal codice creato da altri utenti, oppure è possibile generare eccezioni personalizzate che verranno gestite da altri utenti.

Nota

La versione originale di questo articolo è apparsa sul blog scritto da @KevinMarquette. Il team di PowerShell ringrazia Kevin per averne condiviso il contenuto. È possibile visitare il suo blog all'indirizzo PowerShellExplained.com.

Terminologia di base

Prima di abbordare l'argomento, è necessario illustrare alcuni termini di base.

Eccezione

Un'eccezione è simile a un evento che viene creato quando la normale gestione degli errori non riesce a risolvere il problema. Il tentativo di dividere un numero per zero o una condizione di memoria insufficiente sono esempi di situazioni che creano un'eccezione. A volte l'autore del codice in uso crea eccezioni per determinati problemi quando si verificano.

Generare e intercettare

Quando si verifica un'eccezione, si dice che viene generata un'eccezione. Per gestire un'eccezione generata, è necessario intercettarla. Se un'eccezione viene generata ma non viene intercettata da un elemento, l'esecuzione dello script si arresta.

Stack di chiamate

Lo stack di chiamate è l'elenco delle funzioni che si sono chiamate a vicenda. Quando una funzione viene chiamata, viene aggiunta allo stack o all'inizio dell'elenco. Quando la funzione esce o restituisce un valore, viene rimossa dallo stack.

Quando viene generata un'eccezione, lo stack di chiamate viene controllato per consentire l'intercettazione a un gestore di eccezioni.

Errori fatali e non fatali

Un'eccezione è in genere un errore fatale. Un'eccezione generata viene intercettata o termina l'esecuzione corrente. Per impostazione predefinita, un errore non fatale viene generato da Write-Error e viene aggiunto un errore al flusso di output senza che venga generata un'eccezione.

Si sottolinea questo perché Write-Error e altri errori non fatali non attivano catch.

Ignorare un'eccezione

Questo avviene quando si intercetta un errore solo per eliminarlo. Eseguire questa operazione con cautela perché può rendere molto difficile la risoluzione dei problemi.

Sintassi di base dei comandi

Ecco una rapida panoramica della sintassi di base per la gestione delle eccezioni usata in PowerShell.

Throw

Per creare un evento di eccezione personalizzato, viene generata un'eccezione con la parola chiave throw.

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

Questo crea un'eccezione in fase di runtime che corrisponde a un errore fatale. Viene gestito da 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

Si è già specificato che Write-Error non genera un errore fatale per impostazione predefinita. Se si specifica -ErrorAction Stop, Write-Error genera un errore irreversibile che può essere gestito con un oggetto catch.

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

Grazie a Lee Daily per aver ricordato l'uso di -ErrorAction Stop in questo modo.

Cmdlet -ErrorAction Stop

Se si specifica -ErrorAction Stop in una funzione o un cmdlet avanzato, tutte le istruzioni Write-Error vengono trasformate in errori fatali che interrompono l'esecuzione o che possono essere gestiti da un elemento catch.

Start-Something -ErrorAction Stop

Per altre informazioni sul parametro ErrorAction , vedere about_CommonParameters. Per altre informazioni sulla $ErrorActionPreference variabile, vedere about_Preference_Variables.

Try/Catch

La gestione delle eccezioni in PowerShell (e in molti altri linguaggi) funziona nel modo seguente: si usa try su una sezione di codice; se viene generato un errore, questo viene intercettato con catch. Di seguito è riportato un esempio semplice.

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 se si verifica un errore fatale. Se l'istruzione try viene eseguita correttamente, catch viene ignorata. È possibile accedere alle informazioni sull'eccezione catch nel blocco usando la $_ variabile .

Try/Finally

In alcuni casi non è necessario gestire un errore, ma è comunque necessario eseguire del codice, sia che si verifichi o meno un'eccezione. Uno script finally esegue esattamente questa operazione.

Osservare l'esempio seguente:

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

Ogni volta che si apre una risorsa o ci si connette alla risorsa, è necessario chiuderla. Se ExecuteNonQuery() genera un'eccezione, la connessione non viene chiusa. Di seguito è riportato 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 chiusa anche se non si verificano errori. Lo script finally viene eseguito ogni volta.

Poiché l'eccezione non viene intercettata, viene comunque propagata nello stack di chiamate.

Try/Catch/Finally

È del tutto accettabile usare catch e finally insieme. Nella maggior parte dei casi si userà l'una o l'altra opzione, ma in certi scenari vengono usate entrambe.

$PSItem

Ora che sono state illustrate le nozioni di base, è possibile approfondire il processo.

All'interno del blocco catch è presente una variabile automatica ($PSItem o $_) di tipo ErrorRecord che contiene i dettagli relativi all'eccezione. Ecco una rapida panoramica di alcune 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 si ottiene il messaggio più chiaro da usare nella registrazione e nell'output generale. ToString() viene chiamata automaticamente se $PSItem viene inserita in 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. Ecco InvocationInfo dall'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 visualizzano ScriptName, Line del codice e ScriptLineNumber dove è iniziata la chiamata.

$PSItem.ScriptStackTrace

Questa proprietà visualizza 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

In questo caso si eseguono chiamate solo a funzioni nello stesso script, ma questa proprietà rileva le chiamate anche se sono coinvolti più script.

$PSItem.Exception

Questa è l'eccezione vera e propria che è stata generata.

$PSItem.Exception.Message

Questo è il messaggio generale che descrive l'eccezione ed è un punto di partenza ottimale per la risoluzione dei problemi. La maggior parte delle eccezioni include un messaggio predefinito, ma è possibile impostare 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 in ErrorRecord.

$PSItem.Exception.InnerException

Le eccezioni possono contenere eccezioni interne. Questa situazione si verifica spesso quando il codice che si sta chiamando intercetta un'eccezione e genera un'eccezione diversa. L'eccezione originale viene posizionata all'interno della nuova eccezione.

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

Questo aspetto verrà esaminato più avanti, quando si parlerà di rigenerare le eccezioni.

$PSItem.Exception.StackTrace

Elemento StackTrace per l'eccezione. È stato illustrato un elemento ScriptStackTrace in precedenza, ma questo è destinato alle 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 analisi dello stack si ottiene solo quando l'evento viene generato da codice gestito. Qui si chiama direttamente una funzione di .NET Framework, pertanto questo esempio mostra solo il risultato corrispondente. In genere, quando si esamina un'analisi dello stack, si sta cercando la posizione in cui il codice si arresta e iniziano le chiamate di sistema.

Uso delle eccezioni

Le eccezioni sono molto di più di una sintassi di base e alcune proprietà.

Intercettazione di eccezioni tipizzate

È possibile applicare un grado di selettività alle eccezioni intercettate. 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 verificato 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 IOException viene per prima, è l'eccezione che viene chiamata. Viene chiamato un solo blocco di intercettazione, anche se sono presenti più corrispondenze.

Se avessimo un oggetto System.IO.PathTooLongException, il IOException corrisponderebbe, ma se avessimo un InsufficientMemoryException allora nulla lo catturava e propagava 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 a Redditor u/Sheppard_Ra per aver suggerito questa aggiunta.

Generazione di eccezioni tipizzate

È possibile generare eccezioni tipizzate 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, se si procede in questo modo è necessario specificare un messaggio.

È anche possibile creare una nuova istanza di un'eccezione da generare. Quando si esegue questa operazione il messaggio è facoltativo, perché il sistema ha messaggi predefiniti per tutte le eccezioni incorporate.

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

Se si usa un'eccezione tipizzata, l'utente (o altri utenti) possono intercettare l'eccezione in base al tipo, come indicato nella sezione precedente.

Write-Error -Exception

È possibile aggiungere queste eccezioni tipizzate a Write-Error ed è comunque possibile intercettare (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 intercettarla come segue:

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

Il grande elenco di eccezioni di .NET

Ho compilato un elenco master con l'aiuto della community reddit r/PowerShell che contiene centinaia di eccezioni .NET per integrare questo post.

Per iniziare è opportuno cercare le eccezioni utili per la situazione contingente. È consigliabile provare a usare le eccezioni nello spazio dei nomi System di base.

Le eccezioni sono oggetti

Se si inizia a usare un numero elevato di eccezioni tipizzate, tenere presente che si tratta di oggetti. Eccezioni diverse hanno proprietà e costruttori diversi. Se si esamina la documentazione di FileNotFoundException per System.IO.FileNotFoundException, si noterà che è possibile passare un messaggio e un percorso di file.

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

L'eccezione dispone anche di una proprietà FileName che espone il percorso del file.

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

Per altri costruttori e proprietà dell'oggetto, vedere la documentazione di .NET.

Rigenerazione di un'eccezione

Se nel blocco catch ci si limita a generare (throw) la stessa eccezione, non eseguire catch. È necessario eseguire catch solo per un'eccezione che si prevede di gestire o per la quale si prevede di eseguire un'azione quando si verifica.

In alcuni casi si vuole eseguire un'azione su un'eccezione e quindi rigenerare l'eccezione, in modo che un elemento downstream possa gestirla. È possibile scrivere un messaggio o registrare il problema vicino alla posizione in cui è stato individuato, ma gestire il problema a un livello superiore dello stack.

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

È interessante notare che è possibile chiamare throw dall'interno di catch e questa rigenera 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 si genera una nuova eccezione, la posizione in cui è stata avviata l'eccezione originale viene nascosta.

Rigenerazione di una nuova eccezione

Se si intercetta un'eccezione ma si vuole generarne una diversa, è consigliabile annidare l'eccezione originale all'interno di quella nuova. Questo consente a un utente in un livello inferiore dello stack di accedere all'eccezione come $PSItem.Exception.InnerException.

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

$PSCmdlet.ThrowTerminatingError()

L'unico aspetto non desiderabile nell'uso di throw per le eccezioni non elaborate è il fatto che il messaggio di errore fa riferimento all'istruzione throw e indica che la riga è la posizione in cui si è riscontrato 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.

La visualizzazione di un messaggio di errore indicante che lo script non funziona perché è stata chiamata throw alla riga 31 è un messaggio non gradevole da mostrare agli utenti terzi che useranno lo script. Non comunica nulla di utile.

Dexter Dhami ha rilevato che è possibile usare ThrowTerminatingError() per correggere questa situazione.

$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 stata chiamata all'interno di una funzione Get-Resource, questo è l'errore che verrà 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

Si noterà che fa riferimento alla funzione Get-Resource come origine del problema. Questo offre all'utente un elemento utile.

Poiché $PSItem è un elemento ErrorRecord, è anche possibile usare ThrowTerminatingError in questo modo per rigenerare l'eccezione.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

In questo modo, l'origine dell'errore viene impostata sul cmdlet e gli utenti del cmdlet non vedono gli elementi interni della funzione.

Try può creare errori fatali

Kirk Munro indica che alcune eccezioni sono errori fatali solo quando vengono eseguite all'interno di un blocco try/catch. Questo esempio che ha segnalato genera un'eccezione di runtime di divisione per zero.

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

Quindi la chiama in questo modo per vedere come genera l'errore e restituisce comunque il messaggio.

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

Se tuttavia si inserisce lo stesso codice in una sequenza try/catch, accade qualcosa di diverso.

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

L'errore diventa un errore fatale e il primo messaggio non viene restituito. La parte problematica è il fatto che questo codice può essere incluso in una funzione ma agisce in modo diverso se un utente usa un try/catch.

Non sono stati riscontrati problemi con questa operazione, ma è un caso limite da tenere presente.

$PSCmdlet.ThrowTerminatingError() all'interno di try/catch

Un aspetto particolare di $PSCmdlet.ThrowTerminatingError() è il fatto che crea un errore fatale all'interno del cmdlet, ma questo diventa un errore non fatale al di fuori del cmdlet. In questo modo tocca al chiamante della funzione decidere come gestire l'errore. Può riconvertirlo in un errore fatale usando -ErrorAction Stop o chiamandolo dall'interno di un elemento try{...}catch{...}.

Modelli di funzioni pubbliche

Un ultimo aspetto è l'uso che fa Kirk Munro di try{...}catch{...} prima e dopo ogni beginprocess e il blocco end in tutte le funzioni avanzate. In questi blocchi catch è presente una riga singola che usa $PSCmdlet.ThrowTerminatingError($PSItem) per gestire tutte le eccezioni al di fuori delle funzioni.

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

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

Dato che in queste funzioni tutti gli elementi sono inclusi in un'istruzione try, tutto funziona in modo coerente. Questo approccio presenta anche errori semplici all'utente finale, nascondendo il codice interno dall'errore generato.

Trap

L'analisi si è incentrata sull'aspetto try/catch delle eccezioni. Tuttavia è presente una funzionalità legacy da citare prima di concludere.

Un'istruzione trap viene inserita in uno script o in una funzione per intercettare tutte le eccezioni che si verificano in tale ambito. Quando si verifica un'eccezione, viene eseguito il codice in trap e quindi continua l'esecuzione del codice normale. Se si verificano più eccezioni, trap viene chiamata più volte.

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

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

L'autore non ha mai adottato questo approccio, ma è possibile apprezzarne il valore negli script di amministrazione o del controller che registrano tutte le eccezioni e quindi procedono con l'esecuzione.

Note di chiusura

L'aggiunta di una corretta gestione delle eccezioni agli script non solo li rende più stabili, ma rende anche più semplice la risoluzione delle eccezioni.

È stata dedicata particolare attenzione a throw perché si tratta di un concetto fondamentale nell'ambito della gestione delle eccezioni. PowerShell dispone anche di Write-Error, che gestisce tutte le situazioni in cui si userebbe throw. Quindi dopo la lettura di questo articolo potrebbe non essere più necessario usare throw.

Dopo una descrizione dettagliata della gestione delle eccezioni in questo dettaglio, l'autore passerà a usare Write-Error -Stop per generare errori nel suo codice. Inoltre seguirà i consigli di Kirk Munro e adotterà ThrowTerminatingError come gestore di eccezioni goto per ogni funzione.