Manejo de errores y diseño de reintentos en PowerShell ── de la trampa donde try/catch no funciona a las prácticas recomendadas de exit code y reintentos
· Actualizado el: · Go Komura · PowerShell, Windows, Manejo de errores, Reintentos, Automatización, Mejora operativa, Scripts, Programador de tareas
«El batch nocturno había fallado, pero en el Programador de tareas figuraba como éxito (0x0) y nadie se dio cuenta», «escribí try/catch pero nunca entra en el catch», «se cae una vez al mes por un corte momentáneo de red» ── en cuanto se pone en producción un script de PowerShell, este tipo de consultas llegan sin falta. Entre un script que satisface con solo ejecutarlo a mano y uno que sigue funcionando cada noche sin supervisión, existe una barrera: el diseño del manejo de errores y de la reejecución (reintentos).
Lo complicado es que el modelo de errores de PowerShell difiere sutilmente del modelo de excepciones habitual en otros lenguajes de programación. Que «el proceso continúe aunque se produzca un error» o que «algo que debería haberse capturado con catch se escurra» no suele ser un bug, sino un comportamiento que sigue exactamente la especificación de PowerShell; si se escribe sin conocer este mecanismo, se acaban produciendo en serie «scripts que tragan sus propios fallos y terminan como si todo hubiera ido bien».
Este artículo está dirigido a responsables de sistemas internos y desarrolladores que automatizan procesos rutinarios de la empresa con PowerShell. Repasamos, con el respaldo de la documentación oficial, desde la distinción entre errores terminantes y no terminantes, pasando por la verificación del éxito o fracaso de comandos nativos y el diseño de exit code para que el Programador de tareas o las herramientas de monitorización puedan determinar el resultado, hasta el patrón de reintentos que resiste errores temporales.
1. Conclusión principal
- Los errores de PowerShell se dividen en «errores no terminantes» y «errores terminantes» (de instrucción o de script). Los errores no terminantes muestran un mensaje y la canalización continúa; de forma predeterminada no entran en try/catch. 1
- Hay dos formas de contarlos, así que lo aclaramos primero. La clasificación de la documentación oficial usa tres categorías: no terminante / terminante de instrucción / terminante de script, un eje que refleja «hasta dónde detiene el motor» la ejecución (solo la canalización / solo esa instrucción / toda la pila de llamadas). 1 Por otro lado, lo primero que quiere saber quien escribe el código es si entra en try/catch, y en ese eje solo hay dos tipos: errores no terminantes y errores terminantes. Este artículo usa principalmente el eje de dos tipos y toca el desglose de las tres categorías cuando resulta necesario.
- La práctica recomendada es añadir
-ErrorAction Stopa los comandos que se quieran capturar con try/catch. Stop promueve un error no terminante a error terminante para que pueda tratarse en el catch. También existe la opción de fijar$ErrorActionPreference(cuyo valor predeterminado es Continue) en Stop al principio del script. 12 -ErrorActionsobrescribe$ErrorActionPreferencepara ese comando en particular. Sin embargo, ambos no son totalmente simétricos:-ErrorActionsolo puede controlar errores no terminantes. 1- El fallo de un comando nativo (robocopy, git, un EXE externo) no se convierte en un error de PowerShell de forma predeterminada. Un código de salida distinto de cero pone
$?en$falsey se guarda en$LASTEXITCODE, pero no se crea ningún ErrorRecord ni entra en el catch. El éxito o fracaso se determina con$LASTEXITCODE. 1 - En PowerShell 7.4,
$PSNativeCommandUseErrorActionPreferencepasó a ser una función oficial. Al ponerla en$true, un código de salida distinto de cero genera un error no terminante, y si se combina con$ErrorActionPreference = 'Stop'puede capturarse con try/catch (el valor predeterminado es$false). 32 - Dentro del catch,
$_es el ErrorRecord.$_.Exceptionda acceso al objeto de excepción en sí, y si se trata de un error promovido,$_.Exception.ErrorRecordpermite llegar a la información del error original. Con un bloque catch que especifica el tipo de excepción se pueden tratar de forma individual solo los errores previstos. 14 - El resultado siempre debe comunicarse hacia afuera mediante el exit code. La palabra clave
exitfija el código de salida, y si el script se inicia conpwsh -File/powershell.exe -File, ese valor se convierte en el código de salida del proceso. Sinexit, el resultado es 0 en una finalización normal y 1 ante una excepción no controlada. 56 - Los tres principios del reintento son: limitarlo a errores temporales, ponerle un límite, y que el proceso sea idempotente (propiedad por la que ejecutar la misma operación varias veces no cambia el resultado; se detalla en el capítulo 6). No disimular errores de negocio con reintentos, ampliar el intervalo con backoff exponencial y diseñar el proceso para que la reejecución no provoque un procesamiento duplicado: solo cuando se dan estas tres condiciones el script puede considerarse «apto para reejecutarse».
2. Los dos tipos de error ── por qué try/catch no funciona
Los errores de PowerShell se dividen primero en dos tipos: errores no terminantes y errores terminantes. Y como los errores terminantes se subdividen a su vez en errores terminantes de instrucción y errores terminantes de script, si se cuenta con más detalle resultan tres categorías. Los errores no terminantes solo informan sin detener la canalización; los errores terminantes de instrucción detienen únicamente esa instrucción y continúan con la siguiente; los errores terminantes de script deshacen toda la pila de llamadas. 1
En la práctica, la trampa está en los errores no terminantes. Cuando cmdlets como Get-Content o Get-ChildItem fallan al procesar una entrada concreta, lo habitual es que generen un error no terminante: se muestra el mensaje de error en rojo, pero el proceso continúa y no entra ni en try/catch ni en trap. 1
# [Trampa] Nunca entra en el catch y se llega a mostrar "Completado"
try {
Get-Content -Path 'C:\Data\存在しないファイル.txt' # error no terminante
Write-Host 'Completado' # se ejecuta igualmente
}
catch {
Write-Host 'Esto nunca se ejecuta'
}
# [Práctica recomendada] Promover a error terminante con -ErrorAction Stop para poder tratarlo en el catch
try {
Get-Content -Path 'C:\Data\存在しないファイル.txt' -ErrorAction Stop
Write-Host 'Completado' # no se ejecuta si hay error
}
catch {
Write-Host "Capturado: $($_.Exception.Message)"
}
Cuando -ErrorAction Stop o $ErrorActionPreference = 'Stop' están activos, el motor envuelve el error no terminante en una ActionPreferenceStopException y lo promueve a error terminante. El mecanismo exacto es que, dentro de un bloque try, este error promovido es el que llega al catch. 1
Por otro lado, hay errores que desde el principio son errores terminantes (es decir, entran en el catch sin necesidad de hacer nada especial). Se trata de los errores terminantes de instrucción, y la documentación oficial cita las siguientes fuentes. 1
- Al invocar un comando que no existe (
CommandNotFoundException) - Al fallar el enlace de un parámetro (
ParameterBindingException; por ejemplo, pasar una cadena que no se puede convertir a un parámetro numérico) - Cuando un método de .NET lanza una excepción (por ejemplo,
[int]::Parse('abc')) - Cuando un cmdlet o una función avanzada informa mediante
$PSCmdlet.ThrowTerminatingError()de que «esta llamada ya no puede continuar»
Tal como indica su nombre, solo detiene «esa instrucción», de modo que el script continúa ejecutándose desde la siguiente. 1
# Error terminante de instrucción: esta instrucción se detiene, pero la siguiente se ejecuta
[int]::Parse('abc')
Write-Output 'Esta línea se ejecuta'
# Al ser un error terminante, entra en el catch aunque no se añada -ErrorAction Stop
try { [int]::Parse('abc') }
catch { Write-Warning "Capturado: $($_.Exception.Message)" }
Lo complicado es que, incluso ante una misma situación como «el archivo no existe», según cómo esté implementado el cmdlet puede tratarse de un error no terminante o de un error terminante de instrucción. Distinguirlo cada vez a la hora de escribir el código es poco realista, así que la respuesta práctica consiste en añadir explícitamente -ErrorAction Stop a las líneas que se quieren capturar, uniformando el comportamiento para que, sea cual sea el tipo de error, llegue con seguridad al catch.
La idea de «entonces, ¿por qué no fijar siempre $ErrorActionPreference = 'Stop'?» tiene razón solo a medias. En scripts que se ejecutan sin supervisión, es más seguro detenerse y reportar el fallo que seguir adelante tragándose el error, así que fijar Stop al principio es un buen valor predeterminado. Sin embargo, tenga en cuenta que $ErrorActionPreference afecta a ese ámbito y a los ámbitos secundarios, por lo que también cambia el comportamiento de los módulos o funciones invocados, y que en las tareas de limpieza que pueden fallar sin problema (como borrar archivos temporales) hay que volver a añadir -ErrorAction SilentlyContinue de forma individual. 2
3. Qué leer dentro del catch ── recorriendo el ErrorRecord
El $_ de un bloque catch contiene el ErrorRecord. De ahí se puede obtener la información que conviene dejar registrada en el log. 14
try {
Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
# Con un catch de tipo específico se trata de forma individual solo el "fallo previsto".
# Aunque sea un error promovido, el motor hace coincidir el tipo de excepción original
Write-Warning "Error de E/S: $($_.Exception.Message)"
}
catch {
# Lo inesperado se registra en el log junto con su contexto y se vuelve a lanzar (sin tragárselo)
$rec = $_ # $_ es el ErrorRecord
Write-Warning ('Tipo: {0} / Posición: {1} / Objeto: {2}' -f `
$rec.Exception.GetType().FullName,
$rec.InvocationInfo.PositionMessage,
$rec.TargetObject)
throw # throw sin argumentos propaga el mismo error hacia arriba
}
finally {
# finally se ejecuta tanto si hay éxito como si hay error, e incluso si se detiene con Ctrl+C. La limpieza va aquí
if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}
Hay tres puntos clave que revisar.
$_.Exceptiones el objeto de excepción en sí. Un error promovido con-ErrorAction Stopqueda envuelto enActionPreferenceStopException, pero en el emparejamiento de tipos del catch el motor observa el tipo de excepción original (por ejemplo,ItemNotFoundException), de modo que un catch de tipo específico puede escribirse tal cual. Se puede llegar al ErrorRecord original mediante$_.Exception.ErrorRecord. 1$_.InvocationInfo.PositionMessagecontiene «en qué archivo, en qué línea y con qué comando» ocurrió el error, y en los logs de ejecución desatendida, el tiempo de investigación cambia en órdenes de magnitud según si esta información está presente o no.- El bloque finally se ejecuta tanto si el try tiene éxito, como si hay error, como si se detiene con Ctrl+C. Las tareas de limpieza, como cerrar conexiones o borrar archivos temporales, se colocan en finally. 7
El debate de diseño sobre en qué capa capturar y dónde escribir el log es común entre distintos lenguajes. Los principios recopilados en «Dónde deberían situarse el catch y el log en el manejo de excepciones» (capturar en el límite, no tragarse el error, evitar el registro duplicado) se aplican igual de bien a PowerShell.
4. Éxito o fracaso de comandos nativos ── $?, $LASTEXITCODE y la novedad de 7.4
Otro gran agujero son los comandos nativos, como robocopy, git o los EXE internos de la empresa. Los programas externos no participan del sistema de errores de PowerShell y reportan sus fallos mediante el código de salida. Así es el comportamiento predeterminado. 1
| Evento | Comportamiento (predeterminado) |
|---|---|
| Código de salida distinto de cero | $? pasa a $false y el código de salida se guarda en $LASTEXITCODE |
| Generación de ErrorRecord | No se genera (tampoco se añade a $Error) |
| try/catch | No entra |
Es decir, try { robocopy ... } catch { ... } (de forma predeterminada) no captura nada. La verificación de éxito o fracaso de un comando nativo se escribe con $LASTEXITCODE. $? es un valor booleano que indica «si la operación anterior tuvo éxito», y en el caso de los comandos nativos solo pasa a $true cuando el código de salida es 0. 1 Cabe señalar que en Windows PowerShell 5.1, $? a veces pasaba a $false con solo que el comando nativo escribiera en stderr, pero en PowerShell 7 este comportamiento se cambió para que solo pase a $false cuando el código de salida es distinto de cero. Es un cambio acorde con la realidad de no tratar la salida a stderr como un fallo. 8
# Los comandos nativos se verifican con $LASTEXITCODE
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
# En robocopy, de 0 a 7 son códigos de éxito (información sobre si hubo copia, etc.), 8 o más es fallo
throw "robocopy ha fallado (ExitCode=$LASTEXITCODE)"
}
A partir de PowerShell 7.4 está disponible $PSNativeCommandUseErrorActionPreference, que cambia este comportamiento. Se añadió como función experimental en 7.3 y pasó a ser una función oficial (mainstream) en 7.4. 3 Al ponerla en $true, un comando nativo con código de salida distinto de cero genera un error no terminante que indica explícitamente el código de salida, y este error sigue lo que indique $ErrorActionPreference. Es decir, si se combina con Stop, incluso el fallo de un comando externo puede tratarse con try/catch. 12
En entornos donde conviven 5.1 y 7, confirme siempre la versión de partida. Esta función solo está disponible desde PowerShell 7.4; en 7.3 era una función experimental (con el nombre PSNativeCommandErrorActionPreference), por lo que requería habilitarla con Enable-ExperimentalFeature y reiniciar la sesión. 3 Y en Windows PowerShell 5.1 esta variable simplemente no existe, así que asignarle $true no provoca nada (solo se crea una variable nueva, y como tampoco da error, es fácil pasarlo por alto). Si existe la posibilidad de ejecutar el mismo script tanto en 5.1 como en 7, lo más seguro es no depender de esta función y unificar la verificación mediante $LASTEXITCODE, incluso en los capítulos siguientes.
# PowerShell 7.4 en adelante: tratar también el fallo de comandos externos con try/catch (el valor predeterminado es $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'
try {
git.exe fetch origin
}
catch {
Write-Warning "git ha fallado: $($_.Exception.Message)"
throw
}
& {
# Los comandos como robocopy, donde distinto de cero no significa fallo, se
# desactivan temporalmente dentro del bloque de script y se verifican como antes con
# $LASTEXITCODE (al salir del bloque, vuelve a su valor original)
$PSNativeCommandUseErrorActionPreference = $false
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
if ($LASTEXITCODE -ge 8) { throw "robocopy ha fallado (ExitCode=$LASTEXITCODE)" }
}
Tal como aparece tal cual en la documentación oficial en el ejemplo de robocopy, existen comandos que usan un código de salida distinto de cero como información normal, por lo que si se activa la función de forma general hace falta diseñar los tramos de excepción. 2 En entornos donde solo se puede usar Windows PowerShell 5.1, esta función no existe, así que unifique la verificación con $LASTEXITCODE. Las diferencias de comportamiento entre 5.1 y 7 suelen ser una trampa habitual durante la migración, así que consulte también «Diferencias entre Windows PowerShell 5.1 y PowerShell 7, y cómo migrar».
5. Diseño del exit code ── permitir que el Programador de tareas y la monitorización determinen el resultado
Una vez capturado el error, el siguiente paso es el reporte hacia el exterior. En la práctica, el único medio con el que el Programador de tareas o las herramientas de monitorización conocen el resultado del script es el código de salida del proceso. Repasemos la especificación con precisión.
exit <número>permite indicar explícitamente el código de salida del script.exittambién fija un valor en$LASTEXITCODE. 59- Al iniciar con
pwsh -File(powershell.exe -File), el valor indicado en exit se convierte tal cual en el código de salida del proceso. Si no hay ninguna instrucción exit, el resultado es 0 si finaliza normalmente y 1 si termina por una excepción no controlada. 56 - Si el script se inicia con
-Command, un código de salida comoexit 10dentro del script no se conserva. Se redondea a 0 o 1 según el éxito o fracaso del último comando (si se escribeexit 10directamente en la cadena de comandos, sí se devuelve ese valor). Si la operación distingue entre distintos códigos de salida del script, iniciar con-Filees la práctica recomendada. 6
Si se traduce esta especificación a un esqueleto, la plantilla de un script de ejecución desatendida queda así.
# Invoke-NightlyExport.ps1 ── esqueleto que permite determinar el resultado desde el Programador de tareas
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop' # En la ejecución desatendida, "detenerse y reportar" es el valor predeterminado
# Deja como log el rastro de ejecución, incluyendo salida estándar y errores (-Append añade al archivo diario)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append
try {
Export-DailyData # Cuerpo del proceso de negocio (llama a funciones modularizadas)
exit 0 # Indica explícitamente el éxito
}
catch [System.Net.WebException] {
Write-Warning "Error de comunicación: $($_.Exception.Message)"
exit 10 # Error de tipo temporal ── deja margen para que la tarea configure un reintento
}
catch {
Write-Warning "Error inesperado: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1 # Error permanente ── no reintentar, que lo revise una persona
}
finally {
Stop-Transcript # Con finally, el rastro se cierra incluso al salir mediante exit
}
Start-Transcript es un cmdlet que registra en texto toda la entrada y salida de la sesión, y permite reproducir «qué apareció en pantalla en ese momento» sin necesidad de preparar echo ni redirecciones. 10 No es excluyente respecto a una función de log propia; vale la pena usarlo en conjunto como última línea de defensa. El diseño de logs y las medidas contra su crecimiento excesivo se tratan en «PowerShell aplicado ── automatizar de forma segura la investigación, el archivado y la generación de informes de logs».
El truco al asignar exit codes es no complicarse demasiado. Basta con una granularidad del tipo 0 = éxito, 1 = error permanente (lo revisa una persona), rango de los 10 = error temporal (se puede reintentar), y con eso ya se puede construir directamente la verificación de resultado tanto del «resultado de la última ejecución» del Programador de tareas como de una herramienta de gestión de trabajos. Para la configuración del lado de la tarea (reintento en caso de fallo, forma de comprobar el resultado de la ejecución) consulte «La tarea del Programador de tareas no se ejecuta o termina con 0x1 ── aislamiento de causas y diseño operativo seguro».
6. Diseño de reintentos ── distinguir errores temporales de errores de negocio
Por último, la reejecución. El valor de un reintento es «absorber automáticamente los errores temporales y no despertar a nadie en mitad de la noche», pero si se introduce sin cuidado se generan otros incidentes distintos, como «reintentar indefinidamente un fallo permanente» o «dañar datos por un procesamiento duplicado». Hay tres principios.
- Reintentar solo errores temporales. Limítelo a fallos que el tiempo pueda resolver, como cortes momentáneos de red, bloqueos temporales de archivos o la espera de arranque de un servicio del que depende. Los datos de entrada incorrectos, la falta de permisos o los errores de configuración deben fallar de inmediato, transmitiéndose a las personas mediante el exit code y el log.
- Diseñar un límite y un intervalo. Determine un número máximo de intentos y amplíe el intervalo con backoff exponencial (2 segundos, 4 segundos, 8 segundos…). Seguir golpeando a intervalos fijos a un sistema que está fallando solo entorpece su recuperación.
- Que el proceso sea idempotente (seguro de reejecutar). Tanto un reintento como una reejecución desde el Programador de tareas significan que «el mismo proceso se vuelve a ejecutar». Esto presupone diseños como publicar la salida mediante un archivo temporal más un renombrado, o registrar los ID ya procesados para rechazar una incorporación duplicada.
Como patrón, todo esto puede resumirse en la siguiente forma.
function Invoke-WithRetry {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [scriptblock] $Operation,
# Si se pasa un valor menor o igual a 0, terminaría sin ejecutarse ni una vez, así que se fuerza a 1 o más
[ValidateRange(1, 100)]
[int] $MaxAttempts = 4,
# Un valor negativo provocaría otro error en el Start-Sleep del reintento, así que se rechaza en el enlace
[ValidateRange(0, 3600)]
[int] $BaseDelaySeconds = 2,
# Enumera solo los tipos de excepción que vale la pena reintentar (por defecto, comunicación e E/S)
[Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
try {
# La salida se recibe primero en una variable y se devuelve solo tras el éxito. Si se hace return
# directamente de & $Operation, cuando ocurre una excepción tras haber emitido parte de la salida,
# esa salida parcial llegaría al invocador, y al tener éxito un reintento la misma información llegaría duplicada
$output = & $Operation
return $output
}
catch {
$ex = $_.Exception
$isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
throw # Error de negocio, o límite de reintentos alcanzado ── se deja fallar tal cual
}
# Se pone un límite al tiempo de espera que crece exponencialmente (para no esperar
# demasiado con muchos intentos, y sin superar el rango admitido por Start-Sleep)
$delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
Write-Warning "Fallo (intento ${attempt}): $($ex.Message) ── se reintentará en ${delay} segundos"
Start-Sleep -Seconds $delay
}
}
}
# Uso: la operación en cuestión debe convertirse en error terminante con -ErrorAction Stop
Invoke-WithRetry -Operation {
Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}
# Nota al reintentar Invoke-RestMethod / Invoke-WebRequest en PowerShell 7:
# en la versión 7, un fallo de comunicación llega como una excepción de la familia HttpRequestException
# en lugar de la WebException de la época de 5.1, así que por defecto no se reintenta. Además, una respuesta
# HTTP de error permanente como un 404 llega con el mismo tipo, así que si se recibe una respuesta hay que
# clasificar uno mismo, según el código de estado, si "vale la pena reintentar"
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
# Con -SkipHttpErrorCheck se recibe la respuesta de error sin convertirla en excepción, y se decide según el código
$r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
# Solo los códigos temporales se lanzan como HttpRequestException → se reintentan
throw [System.Net.Http.HttpRequestException]::new("Error HTTP temporal: $($r.StatusCode)")
}
if ($r.StatusCode -ge 400) {
throw "Error HTTP permanente: $($r.StatusCode)" # Al ser de otro tipo, no se reintenta
}
$r.Content | ConvertFrom-Json
}
El punto clave es que se selecciona explícitamente qué reintentar mediante el tipo de excepción. Si se escribe «reintentar todo lo que se capture», incluso un error permanente como un parámetro incorrecto se probaría cuatro veces, generando una espera inútil. Después de poner el script en producción, un enfoque realista consiste en ir añadiendo a $RetryableExceptions los tipos de error temporal observados en los logs reales.
Además, como el cuerpo de Invoke-WithRetry es bastante largo, en lugar de pegarlo en el script cada vez que se usa, guárdelo entero en un archivo como Retry.psm1 y cárguelo con Import-Module. Así se evita el incidente de que, con cada copia y pega, solo el interior del catch termine mezclando una versión antigua (sobre las buenas prácticas de modularización, consulte «Diseño de argumentos y modularización en PowerShell»). Además, la lógica de reintento y de ramificación de errores es precisamente una parte donde vale la pena escribir pruebas con Pester («Mantenimiento de pruebas de PowerShell con Pester ── el patrón práctico para hacer scripts operativos más difíciles de romper»).
7. Prácticas recomendadas (tabla de decisión)
| Punto | Opciones | Criterio de decisión |
|---|---|---|
| Comportamiento predeterminado ante errores | Dejarlo en Continue / $ErrorActionPreference = ‘Stop’ al principio | En la ejecución desatendida es más seguro “detenerse y reportar”. Para scripts interactivos de investigación puede dejarse en Continue 2 |
| Puntos que se quieren capturar | Confiar en la suerte / indicar explícitamente -ErrorAction Stop | Los cmdlets suelen generar errores no terminantes. Indique Stop explícitamente en las líneas que quiera capturar 1 |
| Éxito o fracaso de comandos nativos | Ignorarlo / verificación con $LASTEXITCODE / $PSNativeCommandUseErrorActionPreference de 7.4 | En entornos con mezcla de 5.1, unifique con la verificación de $LASTEXITCODE. Si solo hay 7.4 o superior, use la nueva función + tramos de excepción como robocopy 32 |
| Reporte externo del resultado | Solo log / diseñar el exit code e iniciar con -File | El log es para personas, el exit code es para máquinas. Se necesitan ambos. Iniciar con -Command aplana el código de salida 6 |
| Rastro de ejecución | Solo log propio / usar también Start-Transcript | Un seguro que conserva incluso la salida que el log propio no puede capturar (como la salida estándar de comandos externos) 10 |
| Reintentos | Aplicarlo a todos los errores / limitarlo a errores temporales + backoff exponencial + idempotencia | Reintentar errores de negocio es la causa de incidentes. Con el conjunto de límite, intervalo e idempotencia |
8. Resumen
- Los errores de PowerShell se dividen en errores no terminantes y errores terminantes, y los errores no terminantes no entran en try/catch de forma predeterminada. La práctica recomendada es indicar explícitamente
-ErrorAction Stopen los comandos que se quieran capturar. - El valor predeterminado de
$ErrorActionPreferencees Continue. En los scripts de ejecución desatendida, fíjelo en Stop al principio para evitar de forma estructural el incidente de “tragarse el fallo y terminar como si todo hubiera ido bien”. - El fallo de un comando nativo no entra en el catch de forma predeterminada. Verifíquelo con
$LASTEXITCODE, o aproveche$PSNativeCommandUseErrorActionPreferencesi dispone de PowerShell 7.4 o superior. - En el catch, registre en el log el tipo de excepción, el mensaje y la información de posición a partir de
$_(el ErrorRecord), y coloque la limpieza en finally. El finally se ejecuta incluso con Ctrl+C o exit. - Reporte el resultado hacia el exterior mediante el exit code. Iniciando con
-File, el valor deexitse convierte directamente en el código de salida, y así el Programador de tareas o la monitorización pueden determinar el resultado. - Los reintentos deben regirse por los tres principios: limitarlos a errores temporales, backoff exponencial con límite e idempotencia. Los errores permanentes deben fallar de inmediato y transmitirse a las personas.
Artículos relacionados
- PowerShell aplicado ── automatizar de forma segura la investigación, el archivado y la generación de informes de logs
- Mantenimiento de pruebas de PowerShell con Pester ── el patrón práctico para hacer scripts operativos más difíciles de romper
- La tarea del Programador de tareas no se ejecuta o termina con 0x1 ── aislamiento de causas y diseño operativo seguro
- Dónde deberían situarse el catch y el log en el manejo de excepciones
- Directiva de ejecución y firma de scripts en PowerShell
- Diseño de argumentos y modularización en PowerShell
Áreas de consultoría relacionadas
KomuraSoft LLC se ocupa de la revisión del diseño de manejo de errores y reintentos de batches nocturnos y scripts de procesos rutinarios, de la investigación de fallos intermitentes como «se marca como éxito aunque haya fallado» o «se cae solo una vez al mes», y de la mejora de la calidad operativa de los scripts existentes.
- Consultoría técnica y revisión de diseño
- Investigación de fallos y análisis de causas
- Migración y aprovechamiento de activos existentes
- Contacto
Referencias
-
Microsoft Learn, about_Error_Handling. Sobre las tres categorías de error no terminante, terminante de instrucción y terminante de script; que los errores no terminantes no entran en catch/trap de forma predeterminada; el mecanismo de promoción mediante -ErrorAction Stop (ActionPreferenceStopException y $_.Exception.ErrorRecord); que un catch de tipo específico hace coincidir el tipo de excepción original; las especificaciones de $? y $LASTEXITCODE; que un código de salida distinto de cero de un comando nativo no genera un ErrorRecord de forma predeterminada; y el comportamiento de $PSNativeCommandUseErrorActionPreference. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17
-
Microsoft Learn, about_Preference_Variables. Sobre que el valor predeterminado de $ErrorActionPreference es Continue; que el parámetro -ErrorAction tiene prioridad para cada comando individual; que la configuración se aplica al ámbito y a los ámbitos secundarios; que el valor predeterminado de $PSNativeCommandUseErrorActionPreference es $false; y un ejemplo de desactivación temporal dentro de un bloque de script para comandos como robocopy que usan un código de salida distinto de cero como información. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, What’s New in PowerShell 7.4. Sobre que la función experimental PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) pasó a ser una función oficial (mainstream) en PowerShell 7.4. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Everything you wanted to know about exceptions. Sobre el acceso a la información de la excepción desde $_ dentro de un bloque catch; que los comandos con -ErrorAction Stop y los errores de Write-Error pasan a poder tratarse con catch; y el patrón de liberación de recursos mediante try/finally. ↩ ↩2
-
Microsoft Learn, about_Language_Keywords. Sobre que la palabra clave exit fija el código de salida y también se refleja en $LASTEXITCODE; que un script iniciado con pwsh -File devuelve el argumento numérico de exit como código de salida; y que, sin instrucción exit, el resultado es 0 al completarse con normalidad y 1 ante una excepción no controlada. ↩ ↩2 ↩3
-
Microsoft Learn, about_Pwsh. Sobre cómo se determina el código de salida al iniciar con -File, y que al iniciar con -Command los códigos de salida distintos de 0 y 1 se convierten en 1, por lo que para conservar el código de salida hace falta exit $LASTEXITCODE. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Try_Catch_Finally. Sobre la sintaxis de try/catch/finally, los bloques catch de tipo específico y los catch múltiples, y que el bloque finally se ejecuta tanto en caso de éxito como de error, además de al detenerse con Ctrl+C o al salir con exit dentro de un catch. ↩
-
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sobre que en PowerShell 7 se cambió el comportamiento para que $? no pase a $false por el simple hecho de que un comando nativo escriba en stderr, sino solo cuando el código de salida es distinto de cero. ↩
-
Microsoft Learn, about_Automatic_Variables. Sobre que $LASTEXITCODE guarda el código de salida de un programa nativo o de un script, y que al invocarse con pwsh -File se fija en 1 ante una finalización por excepción, en el valor de la palabra clave exit, o en 0 al completarse con normalidad. ↩
-
Microsoft Learn, Start-Transcript. Sobre el registro en un archivo de texto de los comandos de la sesión y la salida de la consola, la adición mediante -Append, la ubicación y el nombre de archivo predeterminados, y la detención mediante Stop-Transcript. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Dónde mirar cuando un script de PowerShell es lento — claves de arrays, pipeline y cruces de datos
Analizamos las causas típicas de la lentitud en PowerShell: += en arrays, pipeline vs. foreach, cruces con tablas hash, E/S de archivos y...
Deje de usar Write-Host — Flujos de salida de PowerShell y diseño de registros
Explica los seis flujos de salida de PowerShell, los problemas de Write-Host y su uso correcto, por qué se contamina el valor de retorno ...
Procesamiento paralelo en PowerShell — Cuándo usar ForEach-Object -Parallel y cuándo usar jobs
Diferencias entre ForEach-Object -Parallel, Start-ThreadJob y Start-Job, uso de $using:, ThrottleLimit y cuándo el paralelismo resulta má...
Cómo llamar correctamente a un exe externo desde PowerShell — la trampa de las comillas en argumentos, los códigos de salida y la codificación de caracteres
Al llamar a robocopy o a un EXE interno desde PowerShell, los argumentos se corrompen, no se obtiene el código de salida o la salida se v...
El manejo seguro de credenciales en PowerShell — Cómo desterrar las contraseñas en texto plano de los scripts
Organiza el procedimiento para migrar las contraseñas en texto plano de scripts de PowerShell a un almacenamiento seguro: SecureString, D...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué escribí try/catch en PowerShell y aun así no entra en el catch?
- Porque la mayoría de los errores que generan los cmdlets son errores no terminantes (non-terminating error). Lo único que captura try/catch son los errores terminantes: los errores no terminantes muestran un mensaje y el proceso continúa, sin entrar en el catch. La práctica recomendada para solucionarlo es añadir -ErrorAction Stop al comando que se quiere capturar (o fijar $ErrorActionPreference = 'Stop' al principio del script). Esto promueve el error no terminante a error terminante, de modo que pueda tratarse con try/catch.
- ¿Cómo se distingue el uso de $? y $LASTEXITCODE?
- $? es un valor booleano que indica si la operación anterior tuvo éxito, y se fija tanto para cmdlets como para comandos nativos. $LASTEXITCODE es el código de salida del último programa nativo ejecutado (o del script que hizo exit), y no cambia con los errores de un cmdlet. Al determinar el éxito o fracaso de comandos externos como robocopy o git, lo más fiable es hacerlo con $LASTEXITCODE, que además permite comprobar el significado concreto del código de salida. Tenga en cuenta que un código de salida distinto de cero de un comando nativo no entra en el catch de forma predeterminada.
- ¿Cómo se determina desde el Programador de tareas si un script de PowerShell tuvo éxito o no?
- Se indica explícitamente el código de salida con la palabra clave exit al final del script (y en el bloque catch), y del lado de la tarea se inicia con pwsh -File (o powershell.exe -File) y se supervisa el valor de "resultado de la última ejecución". Al iniciar con -File, el valor indicado en exit se convierte tal cual en el código de salida del proceso; sin exit, el resultado es 0 en una finalización normal y 1 ante una excepción no controlada. Si se inicia con -Command, los códigos de salida distintos de 0 y 1 se convierten en 1, así que si se quiere organizar la operación en torno al código de salida, iniciar con -File es la práctica recomendada.
- ¿Ante qué tipo de errores debe realizarse un reintento?
- Se debe limitar a errores temporales cuyo resultado pueda cambiar al reintentar (cortes momentáneos de red, bloqueos temporales de archivos, la espera de arranque de un servicio, etc.). Los errores de negocio o permanentes, como datos de entrada incorrectos, falta de permisos o errores de configuración, fallarán sin importar cuántas veces se intenten, así que no deben reintentarse: hay que hacerlos fallar de inmediato y avisar a las personas mediante el log y el exit code. Incluso cuando se reintenta, es indispensable poner un límite al número de intentos y al intervalo, ampliar el intervalo con backoff exponencial, y diseñar el proceso de forma idempotente para que la reejecución no produzca un procesamiento duplicado.
- ¿Qué hace la configuración $PSNativeCommandUseErrorActionPreference de PowerShell 7.4?
- Es una configuración que genera un error de PowerShell (no terminante) cuando un comando nativo termina con un código de salida distinto de cero. Se añadió como función experimental en PowerShell 7.3 y pasó a ser una función oficial en 7.4 (el valor predeterminado es $false). Al ponerla en $true, sigue lo que indique $ErrorActionPreference, así que si se combina con Stop se puede capturar con try/catch el fallo de un comando externo. Sin embargo, como hay comandos, como robocopy, que usan un código de salida distinto de cero como información normal, hace falta cierto cuidado, como volver a ponerla en $false solo en ese tramo.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.