Tudo o que você queria saber sobre exceções

O tratamento de erros é apenas parte da vida quando se trata de escrever código. Muitas vezes podemos verificar e validar as condições para o comportamento esperado. Quando o inesperado acontece, recorremos ao tratamento de exceções. Você pode facilmente lidar com exceções geradas pelo código de outras pessoas ou pode gerar suas próprias exceções para outras pessoas lidarem.

Nota

A versão original deste artigo apareceu no blog escrito por @KevinMarquette. A equipe do PowerShell agradece Kevin por compartilhar esse conteúdo conosco. Por favor, confira seu blog em PowerShellExplained.com.

Terminologia de base

Precisamos de abordar alguns termos básicos antes de entrarmos neste.

Exceção

Uma exceção é como um evento que é criado quando o tratamento normal de erros não pode lidar com o problema. Tentar dividir um número por zero ou ficar sem memória são exemplos de algo que cria uma exceção. Às vezes, o autor do código que você está usando cria exceções para certos problemas quando eles acontecem.

Jogar e Apanhar

Quando uma exceção acontece, dizemos que uma exceção é lançada. Para lidar com uma exceção lançada, você precisa pegá-la. Se uma exceção é lançada e ela não é capturada por algo, o script para de ser executado.

A pilha de chamadas

A pilha de chamadas é a lista de funções que se chamaram umas às outras. Quando uma função é chamada, ela é adicionada à pilha ou ao topo da lista. Quando a função sai ou retorna, ela é removida da pilha.

Quando uma exceção é lançada, essa pilha de chamadas é verificada para que um manipulador de exceções a capture.

Erros de terminação e não terminação

Uma exceção é geralmente um erro de encerramento. Uma exceção lançada é capturada ou encerra a execução atual. Por padrão, um erro não terminativo é gerado por Write-Error e adiciona um erro ao fluxo de saída sem lançar uma exceção.

Ressalto isso porque Write-Error e outros erros não terminativos não acionam o catch.

Engolir uma exceção

É quando você pega um erro apenas para suprimi-lo. Faça isso com cautela, pois isso pode dificultar muito a solução de problemas.

Sintaxe básica do comando

Aqui está uma visão geral rápida da sintaxe básica de tratamento de exceções usada no PowerShell.

Lançamento

Para criar nosso próprio evento de exceção, lançamos uma exceção com a throw palavra-chave.

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

Isso cria uma exceção de tempo de execução que é um erro de encerramento. Ele é manipulado por um catch em uma função de chamada ou sai do script com uma mensagem como esta.

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 Parar

Eu mencionei que Write-Error não lança um erro de terminação por padrão. Se você especificar -ErrorAction Stop, Write-Error gerará um erro de encerramento que pode ser tratado com um catcharquivo .

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

Obrigado a Lee Dailey por lembrar sobre o uso -ErrorAction Stop desta maneira.

Cmdlet -ErrorAction Stop

Se você especificar -ErrorAction Stop em qualquer função ou cmdlet avançado, ele transformará todas as Write-Error instruções em erros de encerramento que interrompem a execução ou que podem ser manipulados por um catcharquivo .

Start-Something -ErrorAction Stop

Para obter mais informações sobre o parâmetro ErrorAction , consulte about_CommonParameters. Para obter mais informações sobre a $ErrorActionPreference variável, consulte about_Preference_Variables.

Experimentar/Capturar

A maneira como o tratamento de exceções funciona no PowerShell (e em muitos outros idiomas) é que você primeiro try uma seção de código e, se ele gerar um erro, você poderá catch . Aqui está uma amostra rápida.

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

O catch script só é executado se houver um erro de encerramento. Se o try executa corretamente, então ele ignora o catch. Você pode acessar as informações de catch exceção no bloco usando a $_ variável.

Tente/Finalmente

Às vezes, você não precisa lidar com um erro, mas ainda precisa de algum código para executar se uma exceção acontecer ou não. Um finally script faz exatamente isso.

Veja este exemplo:

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

Sempre que abrir ou ligar a um recurso, deve fechá-lo. Se o ExecuteNonQuery() lançamento de uma exceção, a conexão não será fechada. Aqui está o mesmo código dentro de um try/finally bloco.

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

Neste exemplo, a conexão é fechada se houver um erro. Ele também é fechado se não houver erro. O finally script é executado sempre.

Como você não está pegando a exceção, ela ainda é propagada na pilha de chamadas.

Experimente/Captura/Finalmente

É perfeitamente válido para usar catch e finally juntos. Na maioria das vezes, você usará um ou outro, mas poderá encontrar cenários em que usará ambos.

$PSItem

Agora que tiramos o básico do caminho, podemos cavar um pouco mais fundo.

Dentro do catch bloco , há uma variável automática ($PSItem ou $_) do tipo ErrorRecord que contém os detalhes sobre a exceção. Aqui está uma rápida visão geral de algumas das principais propriedades.

Para esses exemplos, usei um caminho inválido para ReadAllText gerar essa exceção.

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

PSItem.ToString()

Isso lhe dá a mensagem mais limpa para usar no registro em log e na saída geral. ToString() é chamado automaticamente se $PSItem for colocado dentro de uma cadeia de caracteres.

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

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

$PSItem.InvocationInfo

Esta propriedade contém informações adicionais coletadas pelo PowerShell sobre a função ou script onde a exceção foi lançada. Aqui está a InvocationInfo exceção de exemplo que criei.

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

Os detalhes importantes aqui mostram o ScriptName, o Line de código e o ScriptLineNumber local onde a invocação começou.

$PSItem.ScriptStackTrace

Esta propriedade mostra a ordem das chamadas de função que o levaram ao código onde a exceção foi gerada.

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

Estou fazendo chamadas para funções apenas no mesmo script, mas isso rastrearia as chamadas se vários scripts estivessem envolvidos.

$PSItem.Exceção

Esta é a verdadeira exceção que foi lançada.

$PSItem.Exception.Message

Esta é a mensagem geral que descreve a exceção e é um bom ponto de partida para a solução de problemas. A maioria das exceções tem uma mensagem padrão, mas também pode ser definida como algo personalizado quando a exceção é lançada.

PS> $PSItem.Exception.Message

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

Esta também é a mensagem retornada ao ligar $PSItem.ToString() se não houver um definido no ErrorRecord.

$PSItem.Exception.InnerException

As exceções podem conter exceções internas. Esse geralmente é o caso quando o código que você está chamando captura uma exceção e lança uma exceção diferente. A exceção original é colocada dentro da nova exceção.

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

Voltarei a esta questão mais tarde, quando falar de voltar a lançar exceções.

$PSItem.Exception.StackTrace

Esta é a StackTrace exceção. Eu mostrei um ScriptStackTrace acima, mas este é para as chamadas para código gerenciado.

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 )

Você só obtém esse rastreamento de pilha quando o evento é lançado do código gerenciado. Estou chamando uma função do .NET Framework diretamente para que seja tudo o que podemos ver neste exemplo. Geralmente, quando você está olhando para um rastreamento de pilha, você está procurando onde seu código para e as chamadas do sistema começam.

Trabalhar com exceções

Há mais exceções do que a sintaxe básica e as propriedades de exceção.

Capturando exceções digitadas

Você pode ser seletivo com as exceções que você pegar. As exceções têm um tipo e você pode especificar o tipo de exceção que deseja capturar.

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

O tipo de exceção é verificado para cada catch bloco até que seja encontrado um que corresponda à sua exceção. É importante perceber que as exceções podem herdar de outras exceções. No exemplo acima, FileNotFoundException herda de IOException. Então, se o fosse o IOException primeiro, então ele seria chamado em vez disso. Apenas um bloco catch é invocado, mesmo se houver várias correspondências.

Se tivéssemos um System.IO.PathTooLongException, o corresponderia IOException mas se tivéssemos um InsufficientMemoryException então nada o pegaria e propagaria a pilha.

Pegue vários tipos ao mesmo tempo

É possível capturar vários tipos de exceção com a mesma catch instrução.

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

Obrigado /u/Sheppard_Ra por sugerir este aditamento.

Lançando exceções digitadas

Você pode lançar exceções digitadas no PowerShell. Em vez de chamar throw com uma cadeia de caracteres:

throw "Could not find: $path"

Use um acelerador de exceção como este:

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

Mas você tem que especificar uma mensagem quando você faz isso dessa forma.

Você também pode criar uma nova instância de uma exceção a ser lançada. A mensagem é opcional quando você faz isso porque o sistema tem mensagens padrão para todas as exceções internas.

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

Se você não estiver usando o PowerShell 5.0 ou superior, deverá usar a abordagem mais antiga New-Object .

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

Usando uma exceção digitada, você (ou outros) pode capturar a exceção pelo tipo, conforme mencionado na seção anterior.

Write-Error -Exception

Podemos adicionar essas exceções digitadas e Write-Error ainda catch podemos os erros por tipo de exceção. Use Write-Error como nestes exemplos:

# 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

Então podemos pegá-lo assim:

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

A grande lista de exceções do .NET

Eu compilei uma lista mestra com a ajuda da comunidade Reddit/r/PowerShell que contém centenas de exceções .NET para complementar este post.

Começo pesquisando nessa lista por exceções que parecem ser adequadas à minha situação. Você deve tentar usar exceções no namespace base System .

As exceções são objetos

Se você começar a usar muitas exceções digitadas, lembre-se de que elas são objetos. Exceções diferentes têm construtores e propriedades diferentes. Se olharmos para a documentação FileNotFoundException do System.IO.FileNotFoundException, veremos que podemos passar uma mensagem e um caminho de arquivo.

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

E ele tem uma FileName propriedade que expõe esse caminho de arquivo.

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

Você deve consultar a documentação do .NET para outros construtores e propriedades do objeto.

Voltar a lançar uma exceção

Se tudo o que você vai fazer no seu catch bloco é throw a mesma exceção, então não catch faça. Você deve apenas catch uma exceção que você planeja manipular ou executar alguma ação quando isso acontecer.

Há momentos em que você deseja executar uma ação em uma exceção, mas relançar a exceção para que algo a jusante possa lidar com ela. Poderíamos escrever uma mensagem ou registrar o problema perto de onde o descobrimos, mas lidar com o problema mais acima na pilha.

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

Curiosamente, podemos chamar throw de dentro do catch e ele relança a exceção atual.

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

Queremos relançar a exceção para preservar as informações de execução originais, como o script de origem e o número da linha. Se lançarmos uma nova exceção neste ponto, ela ocultará onde a exceção começou.

Voltar a lançar uma nova exceção

Se você pegar uma exceção, mas quiser lançar uma diferente, então você deve aninhar a exceção original dentro da nova. Isso permite que alguém abaixo da pilha para acessá-lo como o $PSItem.Exception.InnerException.

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

$PSCmdlet.ThrowTerminatingError()

A única coisa que eu não gosto de usar throw para exceções brutas é que a mensagem de erro aponta para a throw instrução e indica que a linha é onde está o 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.

Ter a mensagem de erro me dizer que meu script está quebrado porque eu liguei throw na linha 31 é uma mensagem ruim para os usuários do seu script verem. Não lhes diz nada de útil.

Dexter Dhami salientou que posso usar ThrowTerminatingError() para corrigir isso.

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

Se assumirmos que ThrowTerminatingError() foi chamado dentro de uma função chamada Get-Resource, então este é o erro que veríamos.

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

Vê como aponta para a Get-Resource função como a fonte do problema? Isso diz ao usuário algo útil.

Porque $PSItem é um ErrorRecord, também podemos usar ThrowTerminatingError esta forma para relançar.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

Isso altera a origem do erro para o Cmdlet e oculta os internos da sua função dos usuários do Cmdlet.

Tentar pode criar erros de terminação

Kirk Munro destaca que algumas exceções são apenas erros de encerramento quando executados dentro de um try/catch bloco. Aqui está o exemplo que ele me deu que gera uma exceção de tempo de execução dividida por zero.

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

Em seguida, invoque-o assim para vê-lo gerar o erro e ainda gerar a mensagem.

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

Mas ao colocar esse mesmo código dentro de um try/catch, vemos outra coisa acontecer.

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

Vemos o erro se tornar um erro de encerramento e não gerar a primeira mensagem. O que eu não gosto sobre este é que você pode ter esse código em uma função e ele age de forma diferente se alguém estiver usando um try/catch.

Eu mesmo não me deparei com problemas com isso, mas é um caso esquisito para estar ciente.

$PSCmdlet.ThrowTerminatingError() dentro de try/catch

Uma nuance é que ele cria um erro de $PSCmdlet.ThrowTerminatingError() encerramento em seu Cmdlet, mas se transforma em um erro de não terminação depois que sai do Cmdlet. Isso deixa o fardo sobre o chamador de sua função para decidir como lidar com o erro. Eles podem transformá-lo novamente em um erro de encerramento usando -ErrorAction Stop ou chamando-o de dentro de um try{...}catch{...}arquivo .

Modelos de função pública

Uma última tomada que tive com minha conversa com Kirk Munro foi que ele coloca um em torno de cada begin, processtry{...}catch{...} e end bloquear em todas as suas funções avançadas. Nesses blocos de captura genéricos, ele tem uma única linha usando $PSCmdlet.ThrowTerminatingError($PSItem) para lidar com todas as exceções deixando suas funções.

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

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

Porque tudo está em uma try declaração dentro de suas funções, tudo age de forma consistente. Isso também fornece erros limpos para o usuário final que oculta o código interno do erro gerado.

Armadilha

Foquei-me no try/catch aspeto das exceções. Mas há um recurso legado que preciso mencionar antes de encerrarmos isso.

A trap é colocado em um script ou função para capturar todas as exceções que acontecem nesse escopo. Quando uma exceção acontece, o código no trap é executado e, em seguida, o código normal continua. Se várias exceções acontecerem, a armadilha é chamada repetidamente.

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

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

Eu pessoalmente nunca adotei essa abordagem, mas posso ver o valor em scripts de administrador ou controlador que registram todas e quaisquer exceções e, em seguida, ainda continuam a executar.

Observações finais

Adicionar o tratamento adequado de exceções aos seus scripts não apenas os torna mais estáveis, mas também facilita a solução de problemas dessas exceções.

Passei muito tempo falando throw porque é um conceito central quando se fala em tratamento de exceções. O PowerShell também nos Write-Error deu que lida com todas as situações em que você usaria throwo . Portanto, não pense que você precisa estar usando throw depois de ler isso.

Agora que dediquei um tempo para escrever sobre o tratamento de exceções neste detalhe, vou passar a usar Write-Error -Stop para gerar erros no meu código. Eu também vou seguir o conselho de Kirk e fazer ThrowTerminatingError meu manipulador de exceções goto para todas as funções.