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 catch
arquivo .
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 catch
arquivo .
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 Redditor u/Sheppard_Ra
por sugerir esta adição.
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
, process
try{...}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 throw
o . 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.