Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
El control de errores es solo parte de la vida cuando se trata de escribir código. A menudo, podemos comprobar y validar las condiciones para el comportamiento esperado. Cuando ocurre lo inesperado, recurrimos 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 otras personas controlen.
Nota:
La versión original de este artículo apareció en el blog escrito por @KevinMarquette. El equipo de PowerShell agradece a Kevin por compartir este contenido con nosotros. Consulte su blog en PowerShellExplained.com.
Terminología básica
Tenemos que cubrir algunos términos básicos antes de saltar a este.
Exception
Una excepción es como un evento que se crea cuando el control de errores normal no puede tratar el problema. Intentar dividir un número por cero o quedarse sin memoria son ejemplos de algo que crea una excepción. A veces, el autor del código que usa crea excepciones para determinados problemas cuando se producen.
Lanzar y capturar
Cuando se produce una excepción, decimos que se lanza una excepción. Para manejar una excepción lanzada, debe capturarla. Si se produce una excepción y algo no lo detecta, el script deja de ejecutarse.
Pila de llamadas
La pila de llamadas es la lista de funciones que se han llamado la una a la otra. 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 retorna, se quita de la pila.
Cuando se produce una excepción, esa pila de llamadas se comprueba para que un controlador de excepciones lo capture.
Errores terminantes y no terminantes
Una excepción suele ser un error de terminación. Una excepción lanzada se captura o termina la ejecución actual. De forma predeterminada, Write-Error genera un error no terminante y agrega un error al flujo de salida sin lanzar una excepción.
Lo señalo porque Write-Error y otros errores no terminantes no desencadenan el catch.
Tragar una excepción
Esto es cuando detecta un error simplemente para suprimirlo. Haga esto con precaución porque puede dificultar la solución de problemas.
Sintaxis de comandos básica
Esta es una introducción rápida a la sintaxis básica de control de excepciones que se usa en PowerShell.
Tirar
Para crear nuestro propio evento de excepción, lanzamos una excepción con la throw palabra clave.
function Start-Something
{
throw "Bad thing happened"
}
Esto crea una excepción en tiempo de ejecución que es un error de terminación. Se controla mediante un catch en una función de llamada o finaliza el script con un mensaje similar al siguiente.
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
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 recordar el uso -ErrorAction Stop de esta manera.
Cmdlet -AcciónDeError Detener
Si especifica -ErrorAction Stop en cualquier función o cmdlet avanzado, convierte todas las instrucciones Write-Error en errores de terminación que detienen la ejecución o que pueden ser manejados por un 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 de $ErrorActionPreference, vea about_Preference_Variables.
Intentar/Capturar
La forma en que el control de excepciones funciona en PowerShell (y muchos otros lenguajes) es que primero se try una sección de código y, si produce un error, puede catch manejarlo. 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 catch script solo se ejecuta si hay un error de terminación. Si el try se ejecuta correctamente, entonces se omite el catch. Puede acceder a la información de excepción en el catch bloque mediante la $_ variable .
Intentar/Finalmente
A veces no es necesario controlar un error, pero aún necesita código para ejecutarse si se produce o no una excepción. Un finally script 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()
Cada vez que abra o conéctese a un recurso, debe cerrarlo. Si ExecuteNonQuery() lanza una excepción, la conexión no se cierra. Este es el mismo código dentro de un try/finally bloque.
$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 se produce un error. También se cierra si no hay ningún error. El finally script se ejecuta cada vez.
Dado que no se captura la excepción, esta sigue propagándose hacia arriba en la pila de llamadas.
Intentar/Capturar/Finalmente
Es perfectamente válido para usar catch y finally juntos. La mayoría de las veces usará una o la otra, pero puede encontrar escenarios en los que se usen ambos.
$PSItem
Una vez que hemos aclarado los conceptos básicos, podemos profundizar un poco más.
Dentro del catch bloque, 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 de algunas de las propiedades clave.
Para estos ejemplos, he usado una ruta de acceso no válida en ReadAllText para generar esta excepción.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
Esto le proporciona el mensaje más limpio que se 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 donde se produjo la excepción. Esta es la 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
Los detalles importantes aquí muestran el ScriptName, el Line de código y el ScriptLineNumber donde se inició la invocación.
$PSItem.ScriptStackTrace
Esta propiedad muestra el orden de las llamadas de función que le llevaron 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 estoy realizando llamadas a funciones en el mismo script, pero esto realizaría un seguimiento de las llamadas si varios scripts estuvieran implicados.
$PSItem.Exception
Esta es la excepción real que se produjo.
$PSItem.Exception.Message (mensaje de la excepción)
Este es el mensaje general que describe la excepción y es un buen punto de partida al solucionar problemas. La mayoría de las excepciones tienen un mensaje predeterminado, pero también se pueden establecer en algo personalizado cuando se produce la excepción.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
Este es también el mensaje devuelto al llamar a $PSItem.ToString() si no había uno establecido en ErrorRecord.
$PSItem.Exception.InnerException
Las excepciones pueden contener excepciones internas. Este suele ser el caso cuando el código al que llama captura una excepción y lanza 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.
Volveré a revisar esto más adelante cuando hable sobre el relanzamiento de excepciones.
$PSItem.Exception.StackTrace
Este es el StackTrace para la excepción. He mostrado un ScriptStackTrace arriba, pero este es para las llamadas al 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 pila cuando se produce el evento desde código administrado. Estoy llamando directamente a una función de .NET Framework para que sea todo lo que podemos ver en este ejemplo. Por lo general, cuando examina un seguimiento de pila, busca dónde se detiene el código y comienzan las llamadas del sistema.
Trabajar con excepciones
Hay más excepciones que la sintaxis básica y las propiedades de excepción.
Captura de excepciones tipadas
Puede escoger las excepciones que detecta. Las excepciones tienen un tipo y puede especificar el tipo de excepción que desea detectar.
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 catch bloque hasta que se encuentre uno que coincida con la excepción.
Es importante tener en cuenta que las excepciones pueden heredar de otras excepciones. En el ejemplo anterior, FileNotFoundException hereda de IOException. Por lo tanto, si el IOException estuviera primero, se llamaría en su lugar. 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 capturaría y se propagaría en la pila.
Capturar varios tipos simultáneamente
Es posible detectar varios tipos de excepción con la misma catch instrucción.
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 a Redditor u/Sheppard_Ra por sugerir esta adición.
Lanzar excepciones tipadas
Puede lanzar excepciones tipificadas 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"
Pero tiene que especificar un mensaje cuando lo haga de esa manera.
También puede crear una nueva instancia de una excepción que se va a producir. El mensaje es opcional cuando lo hace 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 superior, debe usar el enfoque anterior New-Object .
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
Al utilizar una excepción tipada, tú (u otros) pueden capturar la excepción por su tipo, tal como se mencionó en la sección anterior.
Write-Error -Exception
Podemos agregar estas excepciones tipadas a Write-Error y todavía podemos catch 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()
}
Lista grande de excepciones de .NET
He compilado una lista maestra con la ayuda de la comunidad de Reddit r/PowerShell que contiene cientos de excepciones de .NET para complementar esta publicación.
Empiezo por buscar en esa lista las excepciones que parecen adecuadas para mi situación. Debe intentar usar excepciones en el espacio de nombres base System.
Las excepciones son objetos
Si empieza a usar muchas excepciones tipadas, recuerde que son objetos . Las distintas excepciones tienen constructores y propiedades diferentes. Si examinamos la documentación de FileNotFoundException para System.IO.FileNotFoundException, vemos que podemos pasar un mensaje y una ruta de archivo.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
Y posee la propiedad FileName que expone esa ruta de acceso de archivo.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
Debe consultar la documentación de .NET para ver otros constructores y propiedades de objeto.
Volver a lanzar una excepción
Si todo lo que va a hacer en el catch bloque es throw la misma excepción, no catch lo haga. Solo catch debe tener una excepción que planee controlar o realizar alguna acción cuando se produzca.
Hay ocasiones en las que desea realizar una acción en una excepción, pero relanzar la excepción para que algo posterior pueda manejarla. Podríamos escribir un mensaje o registrar el problema cerca de donde lo descubrimos, pero controlar el problema más arriba de la pila.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
Es interesante que podemos llamar a throw desde dentro de catch y se vuelve a lanzar 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 iniciamos una nueva excepción en este punto, oculta dónde se inició la excepción.
Volver a lanzar una nueva excepción
Si detecta una excepción, pero desea iniciar una diferente, debe anidar la excepción original dentro de la nueva. Esto permite que alguien en niveles inferiores de la pila acceda a ella como el $PSItem.Exception.InnerException.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
Lo único que no me gusta al usar throw para excepciones sin procesar es que el mensaje de error apunta a la throw instrucción e indica que esa línea es donde está 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.
Ver el mensaje de error que dice que mi script está roto porque llamé throw en la línea 31 es un mensaje poco conveniente para los usuarios de tu 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 suponemos que ThrowTerminatingError() se llamó dentro de una función denominada Get-Resource, este es el error 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
¿Ve cómo apunta a la Get-Resource función como origen del problema? Esto indica al usuario algo útil.
Dado que $PSItem es un ErrorRecord, también podemos usar ThrowTerminatingError de esta manera para relanzar.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Esto cambia el origen del error al Cmdlet y oculta los elementos internos de tu función de los usuarios de tu Cmdlet.
Pruebe a crear errores de terminación
Kirk Munro señala que algunas excepciones solo están finalizando errores cuando se ejecutan dentro de un try/catch bloque. Este es el ejemplo que me dio que genera una excepción de división por cero en tiempo de ejecución.
function Start-Something { 1/(1-1) }
Luego, invóquelo así para ver que genera el error y aún así genera el mensaje.
&{ Start-Something; Write-Output "We did it. Send Email" }
Pero al colocar ese mismo código dentro de , try/catchvemos que sucede algo más.
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 este es que puede tener este código en una función y actúa de forma diferente si alguien usa un try/catch.
No me he encontrado problemas con esto yo mismo, pero es un caso extremo a tener en cuenta.
$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 que salga del Cmdlet. Esto deja la carga en quien llama a tu función para decidir cómo manejar el error. Pueden volver a convertirlo en un error de terminación mediante -ErrorAction Stop o llamándolo desde dentro de try{...}catch{...}.
Plantillas de función pública
Una última conclusión que tuve de mi conversación con Kirk Munro fue que él coloca un 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 usando $PSCmdlet.ThrowTerminatingError($PSItem) para manejar todas las excepciones que salen de sus funciones.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Porque todo está en una declaración try dentro de sus funciones, todo actúa de forma coherente. Esto también proporciona errores limpios al usuario final que ocultan el código interno del error generado.
Trampa
Me he centrado en el try/catch aspecto de las excepciones. Pero hay una característica heredada que necesito mencionar antes de encapsular esto.
Se coloca trap en un script o función para capturar todas las excepciones que ocurren en ese ámbito. Cuando se produce una excepción, se ejecuta el código de trap y, a continuación, el código normal continúa. Si se producen varias excepciones, se llama al gestor de excepciones 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 he adoptado este enfoque, pero puedo ver el valor en scripts de administrador o controlador que registran todas y todas las excepciones y, a continuación, siguen ejecutándose.
Comentarios de cierre
Agregar un 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 throw porque es un concepto básico al hablar sobre el control de excepciones. PowerShell también nos dio Write-Error que gestiona todas las situaciones en las que usaría throw. Así que no pienses que debes estar usando throw después de leer esto.
Ahora que me he tomado el tiempo para escribir sobre el control de excepciones con este detalle, voy a pasar a usar Write-Error -Stop para generar errores en mi código. También voy a tomar el consejo de Kirk y hacer ThrowTerminatingError mi controlador de excepciones goto para cada función.