Todo lo que quiso saber sobre las excepciones
El control de errores es solo parte de la vida cuando se trata de escribir código. A menudo, se pueden comprobar y validar las condiciones del comportamiento esperado. Cuando ocurre lo inesperado, acudimos al control de excepciones. Puede controlar fácilmente las excepciones generadas por el código de otras personas o puede generar sus propias excepciones para que las controlen otros usuarios.
Nota:
La versión original de este artículo apareció en el blog escrito por @KevinMarquette. El equipo de PowerShell agradece a Kevin que comparta este contenido con nosotros. Visite su blog en PowerShellExplained.com.
Terminología básica
Es necesario tratar algunos términos básicos antes de pasar a este.
Excepción
Una excepción es como un evento que se crea cuando el control de errores normal no puede resolver el problema. Intentar dividir un número por cero o quedarse sin memoria son ejemplos de situaciones que crean una excepción. En ocasiones, el autor del código que se usa crea excepciones para determinados problemas cuando se producen.
Throw (iniciar) y Catch (capturar)
Cuando se produce una excepción, decimos que se inicia una excepción. Para controlar una excepción iniciada, debe capturarla. Si se inicia una excepción y no se captura con algo, el script deja de ejecutarse.
La pila de llamadas
La pila de llamadas es la lista de funciones que se han llamado entre sí. Cuando se llama a una función, se agrega a la pila o a la parte superior de la lista. Cuando la función termina o devuelve un resultado, se quita de la pila.
Cuando se inicia una excepción, se comprueba esa pila de llamadas para que un controlador de excepciones la capture.
Errores de terminación y no terminación
Por lo general, una excepción es un error de terminación. Una excepción iniciada se captura o termina la ejecución actual. De forma predeterminada, Write-Error
genera un error de no terminación y agrega un error al flujo de salida sin iniciar una excepción.
Conviene destacar este punto porque Write-Error
y otros errores de no terminación no desencadenan una operación catch
.
Tragarse una excepción
Es cuando se captura un error solo para suprimirlo. Hágalo con precaución, ya que puede dificultar la solución de problemas.
Sintaxis básica de comandos
Esta es una introducción rápida a la sintaxis básica de control de excepciones que se usa en PowerShell.
Throw
Para crear nuestro propio evento de excepción, iniciamos una excepción con la palabra clave throw
.
function Start-Something
{
throw "Bad thing happened"
}
Esta acción crea una excepción en tiempo de ejecución que es un error de terminación. Se controla mediante una operación catch
en una función de llamada o sale del script con un mensaje similar a este.
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
He mencionado que Write-Error
no produce un error de terminación de forma predeterminada. Si especifica -ErrorAction Stop
, Write-Error
genera un error de terminación que se puede controlar con catch
.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
Gracias a Lee Dailey por recordarnos el uso de -ErrorAction Stop
de esta manera.
Cmdlet -ErrorAction Stop
Si especifica -ErrorAction Stop
en cualquier función o cmdlet avanzados, todas las instrucciones Write-Error
se convierten en errores de terminación que detienen la ejecución o que se pueden controlar mediante catch
.
Start-Something -ErrorAction Stop
Para obtener más información sobre el parámetro ErrorAction, consulte about_CommonParameters. Para obtener más información sobre la variable $ErrorActionPreference
, vea about_Preference_Variables.
Try/Catch
La forma en que funciona el control de excepciones en PowerShell (y muchos otros lenguajes) es que primero se usa try
para probar una sección del código y, si se produce un error, puede usar catch
para capturarlo. Este es un ejemplo rápido.
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 $_
}
El script catch
solo se ejecuta si se produce un error de terminación. Si try
se ejecuta correctamente, se omite catch
. Puede acceder a la información sobre la excepción del bloque catch
usando la variable $_
.
Try/Finally
En ocasiones, no es necesario controlar un error, pero sí es necesario ejecutar código tanto si se produce una excepción como si no. Un script finally
hace exactamente eso.
Eche un vistazo a este ejemplo:
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
Siempre que abra o se conecte a un recurso, debe cerrarlo. Si ExecuteNonQuery()
inicia una excepción, la conexión no se cierra. Este es el mismo código dentro de un bloque try/finally
.
$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
En este ejemplo, la conexión se cierra si hay un error. También se cierra si no hay ningún error. El script finally
se ejecuta cada vez.
Como no se captura la excepción, se sigue propagando a la pila de llamadas.
Try/Catch/Finally
Es absolutamente válido usar juntos catch
y finally
. Casi siempre usará uno u otro, pero puede encontrarse con escenarios en los que se usen ambos.
$PSItem
Ahora que tenemos los aspectos básicos, podemos escarbar un poco más.
Dentro del bloque catch
, hay una variable automática ($PSItem
o $_
) de tipo ErrorRecord
que contiene los detalles sobre la excepción. Esta es una introducción rápida a algunas de las propiedades principales.
En estos ejemplos, usé una ruta de acceso no válida en ReadAllText
para generar esta excepción.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
Proporciona el mensaje más limpio para usar en el registro y la salida general. ToString()
se llama automáticamente si $PSItem
se coloca dentro de una cadena.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
Esta propiedad contiene información adicional recopilada por PowerShell sobre la función o el script en el que se inició la excepción. Este es el elemento InvocationInfo
de la excepción de ejemplo que he creado.
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
Aquí, los detalles importantes muestran el elemento ScriptName
, el elemento Line
del código y el elemento ScriptLineNumber
en el que se inició la invocación.
$PSItem.ScriptStackTrace
Esta propiedad muestra el orden de las llamadas de función que le han llevado al código donde se generó la excepción.
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
Solo hago llamadas a funciones del mismo script, pero si se invocaran varios scripts, se realizaría un seguimiento de las llamadas.
$PSItem.Exception
Esta es la excepción real que se inició.
$PSItem.Exception.Message
Este es el mensaje general que describe la excepción y es un buen punto de partida para la solución de problemas. La mayoría de las excepciones tienen un mensaje predeterminado, pero también se puede establecer en algo personalizado cuando se inicia la excepción.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Este también es el mensaje que se devuelve al llamar a $PSItem.ToString()
si no hay uno establecido en ErrorRecord
.
$PSItem.Exception.InnerException
Las excepciones pueden contener excepciones internas. Este suele ser el caso cuando el código al que se llama captura una excepción e inicia una excepción diferente. La excepción original se coloca dentro de la nueva excepción.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
Más adelante volveremos a tratar este tema al hablar sobre el inicio repetido de excepciones.
$PSItem.Exception.StackTrace
Es el elemento StackTrace
de la excepción. Anteriormente mostré un elemento ScriptStackTrace
, pero este es para las llamadas a código administrado.
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 )
Solo se obtiene este seguimiento de la pila cuando se inicia el evento desde código administrado. Voy a llamar directamente a una función de .NET Framework, así que eso es todo lo que podemos ver en este ejemplo. Por lo general, cuando se examina un seguimiento de la pila, se busca dónde se detiene el código y comienza la llamada del sistema.
Trabajo con excepciones
Hay más excepciones que las propiedades básicas de excepción y sintaxis.
Captura de excepciones con tipo
Puede ser selectivo con las excepciones que capture. Las excepciones tienen un tipo y puede especificar el tipo de excepción que desea 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"
}
El tipo de excepción se comprueba para cada bloque catch
hasta que se encuentre uno que coincida con la excepción.
Es importante saber que las excepciones pueden heredar de otras excepciones. En el ejemplo anterior, FileNotFoundException
hereda de IOException
. Por lo tanto, si IOException
fue primero, esta sería la excepción a la que se llamaría. Solo se invoca un bloque catch aunque haya varias coincidencias.
Si tuviéramos un System.IO.PathTooLongException
, el IOException
coincidiría pero si tuviéramos un InsufficientMemoryException
entonces nada lo detectaría y se propagaría la pila.
Captura de varios tipos a la vez
Es posible capturar varios tipos de excepción con la misma instrucción 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]"
}
Gracias Redditor u/Sheppard_Ra
por sugerir esta adición.
Inicio de excepciones con tipo
Puede iniciar excepciones con tipo en PowerShell. En lugar de llamar a throw
con una cadena:
throw "Could not find: $path"
Use un acelerador de excepciones como este:
throw [System.IO.FileNotFoundException] "Could not find: $path"
Sin embargo, tendrá que especificar un mensaje cuando lo haga.
También puede crear una instancia de una excepción que se va a iniciar. El mensaje es opcional cuando hace esto porque el sistema tiene mensajes predeterminados para todas las excepciones integradas.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
Si no usa PowerShell 5.0 o posterior, debe utilizar el método New-Object
anterior.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
Mediante una excepción con tipo, usted (u otros) puede capturar la excepción por el tipo, como se ha mencionado en la sección anterior.
Error de escritura: excepción
Podemos agregar estas excepciones con tipo a Write-Error
y todavía podemos usar catch
para capturar los errores por tipo de excepción. Use Write-Error
como en estos ejemplos:
# 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
A continuación, podemos capturarlo de la siguiente manera:
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
La lista grande de excepciones de .NET
He compilado una lista maestra con la ayuda de la comunidad Reddit r/PowerShell
que contiene cientos de excepciones de .NET para complementar esta publicación.
Comencemos por buscar en esa lista las excepciones que podrían ser una buena opción para mi situación. Debe intentar usar excepciones en el espacio de nombres System
base.
Las excepciones son objetos
Si empieza a usar muchas excepciones con tipo, recuerde que son objetos. Diferentes excepciones tienen diferentes constructores y propiedades. Si examinamos la documentación de FileNotFoundException para System.IO.FileNotFoundException
, vemos que se puede pasar un mensaje y una ruta de acceso de archivo.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
Además, tiene una propiedad FileName
que expone esa ruta de acceso de archivo.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
Consulte la documentación de .NET para información sobre otros constructores y propiedades de objeto.
Inicio repetido de una excepción
Si todo lo que va a hacer en el bloque catch
es la acción throw
con la misma excepción, no use catch
con ella. Solo debe usar catch
con una excepción que planee controlar o si va a realizar alguna acción cuando se produzca.
Hay ocasiones en las que querrá realizar una acción en una excepción, pero volver a iniciar esta para que un elemento de nivel inferior pueda encargarse de ella. Podríamos escribir un mensaje o registrar el problema cerca de donde lo detectamos, pero tratarlo mejor arriba de la pila.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Curiosamente, podemos llamar a throw
desde dentro de catch
y volver a iniciar la excepción actual.
catch
{
Write-Log $PSItem.ToString()
throw
}
Queremos volver a iniciar la excepción para conservar la información de ejecución original como el script de origen y el número de línea. Si se inicia una nueva excepción en este punto, se oculta dónde comenzó.
Inicio repetido de una nueva excepción
Si captura una excepción, pero quiere iniciar una diferente, debe anidar la excepción original dentro de la nueva. De esta manera, cualquier usuario de debajo de la pila puede acceder a ella como $PSItem.Exception.InnerException
.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
Lo que no me gusta de usar throw
para las excepciones sin procesar es que el mensaje de error señala a la instrucción throw
e indica que la línea es el lugar donde se encuentra el 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.
Recibir el mensaje de error me dice que el script se ha interrumpido porque llamé a throw
en la línea 31 y este es un buen mensaje que deban ver los usuarios del script. No les dice nada útil.
Dexter Dhami señaló que puedo usar ThrowTerminatingError()
para corregirlo.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Si damos por hecho que se llamó a ThrowTerminatingError()
dentro de una función llamada Get-Resource
, este es el error que veremos.
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
¿Ve cómo apunta a la función Get-Resource
como el origen del problema? Esto indica al usuario algo útil.
Dado que $PSItem
es un elemento ErrorRecord
, también podemos usar ThrowTerminatingError
de esta manera para volver a iniciarla.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Esto cambia el origen del error al cmdlet y oculta los elementos internos de la función a los usuarios del cmdlet.
Try puede crear errores de terminación
Kirk Munro señala que algunas excepciones son solo errores de terminación cuando se ejecutan dentro de un bloque try/catch
. Este es el ejemplo que él me proporcionó que genera una excepción de tiempo de ejecución de división por cero.
function Start-Something { 1/(1-1) }
Luego, se invoca así para ver cómo se genera el error y se envía el mensaje.
&{ Start-Something; Write-Output "We did it. Send Email" }
Sin embargo, al colocar ese mismo código dentro de un bloque try/catch
, vemos que sucede otra cosa.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
Vemos que el error se convierte en un error de terminación y no genera el primer mensaje. Lo que no me gusta de esto es que puede tener este código en una función y actúa de forma diferente si alguien usa try/catch
.
No he tenido problemas con esto, pero es una situación que puede darse.
$PSCmdlet.ThrowTerminatingError() dentro de try/catch
Un matiz de $PSCmdlet.ThrowTerminatingError()
es que crea un error de terminación dentro del cmdlet, pero se convierte en un error de no terminación después de salir de él. Esto deja en manos del autor de la llamada de la función la decisión de cómo controlar el error. Se puede volver a convertir en un error de terminación mediante -ErrorAction Stop
o si se llama desde try{...}catch{...}
.
Plantillas de función públicas
Una última conclusión que saque de mi conversación con Kirk Munro era que coloca un elemento try{...}catch{...}
alrededor de cada bloque begin
, process
y end
en todas sus funciones avanzadas. En esos bloques catch genéricos, tiene una sola línea que usa $PSCmdlet.ThrowTerminatingError($PSItem)
para tratar todas las excepciones que abandonan sus funciones.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Dado que todo está en una instrucción try
dentro de sus funciones, todo funciona de forma coherente. Esto también proporciona al usuario final errores limpios que ocultan el código interno del error generado.
Trap
Me he centrado en el aspecto try/catch
de las excepciones. Pero hay una característica heredada que es necesario mencionar antes de terminar esta parte.
Un elemento trap
se coloca en un script o en una función para capturar todas las excepciones que se producen en ese ámbito. Cuando se produce una excepción, se ejecuta el código de trap
y, luego, continúa el código normal. Si se producen varias excepciones, se llama a la captura una y otra vez.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
Personalmente, nunca adopté este enfoque, pero puedo ver el valor en los scripts de administrador o controlador que registran todas las excepciones y, luego, se siguen ejecutando.
Comentarios de cierre
Agregar el control de excepciones adecuado a los scripts no solo hace que sean más estables, sino que también facilita la solución de problemas de esas excepciones.
Pasé mucho tiempo hablando de throw
porque es un concepto básico al hablar sobre el control de excepciones. PowerShell también nos ha proporcionado Write-Error
que controla todas las situaciones en las que usaría throw
. Por lo tanto, no debe pensar que necesita usar throw
después de leer esto.
Ahora que he tenido tiempo de escribir sobre el control de excepciones en este detalle, voy a cambiar al uso de Write-Error -Stop
para generar errores en mi código. También voy a seguir los consejos de Kirk y a hacer de ThrowTerminatingError
mi controlador de excepciones goto para cada función.