Manejo de errores terminantes en PowerShell
Abstract
Al terminar esta lección podrás controlar el flujo con errores terminantes en
PowerShell: promover fallos con ,
capturarlos con -ErrorAction Stop -ErrorAction Stop /try try /
catch catch y devolver salida estructurada en lugar de
texto suelto. Nos centraremos en errores terminantes; los errores
no terminantes aparecerán solo como contraste y los estudiaremos en detalle más
adelante, cuando entremos en el pipeline.
finally finally
También verás un patrón para ejecutar comandos externos de forma segura: resolver
el binario real con , invocarlo con
Get-Command Get-Command , capturar salida y errores (& & ), validar 2>&1 2>&1 y elevar con contexto si falla
(por ejemplo, mediante un wrapper como
$LASTEXITCODE $LASTEXITCODE ). El ejercicio final te invita a practicar
estos conceptos implementando un fallback simple entre dos comandos de descarga.
Invoke-Tool.ps1 Invoke-Tool.ps1
Errores terminantes
En un shell orientado a automatización y pipelines como PowerShell, no todos los fallos
deben detener la ejecución: a menudo procesas muchos elementos (archivos, filas,
recursos remotos) y necesitas registrar errores por ítem sin abortar el lote
completo (errores no terminantes). En cambio, cuando una operación es crítica (p.
ej., escribir/borrar datos relevantes), conviene elevar el error a terminante
para detener el flujo y manejarlo con /try try . PowerShell modela explícitamente esta distinción y te da controles como
catch catch y -ErrorAction -ErrorAction para escalar fallos no terminantes a terminantes cuando lo requiera el guion,
mientras que $ErrorActionPreference $ErrorActionPreference /try try solo
captura errores terminantes. Esto permite equilibrar robustez en lote y seguridad en operaciones críticas dentro del mismo lenguaje.
catch catch
Error no terminante
Un error no terminante registra la falla pero continúa la ejecución. Es útil cuando procesas un conjunto de elementos y quieres obtener un reporte de qué funcionó y qué falló, sin detener todo ante el primer error. Solo lo mencionamos aquí como contexto; lo veremos con más detalle cuando abordemos el pipeline, donde resulta especialmente relevante.
Error terminante
Un error terminante interrumpe inmediatamente el flujo. Úsalo cuando
la operación es crítica (crear/escribir/borrar datos relevantes, cambios de estado,
CI/CD), promoviendo fallos con
y manejándolos con -ErrorAction Stop -ErrorAction Stop /try try /catch catch .
finally finally
Este ejemplo implementa una copia segura con y elevación de errores a terminantes para capturarlos con
ShouldProcess ShouldProcess . El bloque catch catch siempre
devuelve un objeto con estado de la operación.
finally finally
ShouldProcess y salida estructurada #Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[ValidateScript({ Test-Path -LiteralPath $_ })]
[string] $Source,
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string] $Destination,
[switch] $Recurse
)
Set-StrictMode -Version 3.0
$result = @{
Source = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Source)
Destination = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Destination)
}
$result.Target = Join-Path $result.Destination (Split-Path -Leaf $result.Source)
try {
$shouldProcess = $PSCmdlet.ShouldProcess($result.Source, 'Copy to {0}' -f $result.Destination)
if ($shouldProcess) {
if (Test-Path -LiteralPath $result.Target) {
$result.Status = 'Skipped'
$result.Reason = ('Target already exists: {0}' -f $result.Target)
}
else {
$copyParams = @{
LiteralPath = $result.Source
Destination = $result.Destination
Recurse = $Recurse
ErrorAction = 'Stop'
}
Copy-Item @copyParams
$result.Status = 'Copied'
}
}
else {
# Make cancellation explicit so callers don't need to infer it from missing Status
$result.Status = 'Cancelled'
$result.Reason = 'ShouldProcess returned False (confirmation declined or -WhatIf)'
}
}
catch {
$result.Status = 'Failed'
$result.Error = [PSCustomObject]@{
Kind = $_.Exception.GetType().Name
Message = $_.Exception.Message
}
}
finally {
[PSCustomObject]$result
} ¿Qué acabamos de hacer?
- Rutas normalizadas y destino final:
resuelve rutas absolutas;$PSCmdlet.GetUnresolvedProviderPathFromPSPath()$PSCmdlet.GetUnresolvedProviderPathFromPSPath()compone elJoin-Path ... (Split-Path -Leaf ...)Join-Path ... (Split-Path -Leaf ...)Target. - Validación temprana:
asegura que el origen existe y que el destino es un contenedor.[ValidateScript({ Test-Path ... })][ValidateScript({ Test-Path ... })] -
: promueve fallos de-ErrorAction Stop-ErrorAction Stopa terminantes para entrar enCopy-ItemCopy-Item.catchcatch - Salida estructurada: en
devolvemosfinallyfinallycon$result$resultSource/Destination/Target,Statusy, cuando aplica,ReasonoError.
Uso
Ejecuta el script desde la terminal pasando un archivo y una carpeta destino:
# Preparación mínima
$dest = 'backup'
if (!(Test-Path -LiteralPath $dest)) {
New-Item -ItemType Directory -Path $dest
}
# Ensayo con -WhatIf -> 'Cancelled'
$w = ./Copy-ItemStrict.ps1 -Source './Copy-ItemStrict.ps1' -Destination $dest -WhatIf
$w.Status
$w.Reason
# Copia real -> 'Copied' (primera vez) o 'Skipped'
$r = ./Copy-ItemStrict.ps1 -Source './Copy-ItemStrict.ps1' -Destination $dest
$r.Status
$r.Target Piensa rápido
- Repite la copia para observar
Skippedy elReason. - Intenta copiar una carpeta sin
-Recursepara verFailedy leerError.Kind/Error.Message.
Esquema de salida y estabilidad
Cuando se omite, copia o falla, el ejemplo adjunta
Status y, según el caso, Reason o Error. Si
es rechazado (confirmación o
ShouldProcess ShouldProcess ), aquí marcamos -WhatIf -WhatIf Cancelled de forma
explícita. Una alternativa más simple es devolver solo el objeto base (Source/Destination/Target) sin Status, pero eso varía el
esquema entre ramas.
Decide qué prefieres para tus consumidores: un esquema estable/tabular (mismas propiedades siempre, ideal para filtrar/seleccionar en pipeline) o uno más flexible/JSON (propiedades opcionales según el caso). En estas notas todavía no trabajamos con el pipeline, así que priorizamos claridad y registro por sobre la rigidez del esquema.
Nota: al construir con @ (hashtable) se puede perder el
orden de propiedades; si necesitas orden estable (p. ej., para salida
tabular), puedes crear un desde el
comienzo y agregar propiedades con [PSCustomObject] [PSCustomObject] . Aquí
elegimos la opción más simple con fines ilustrativos.
Add-Member Add-Member
catch catch tipado
catch catch tipado
Puedes definir varios bloques para distintos
tipos de excepción y cerrar con uno genérico. Ordénalos de los más específicos a los
más generales.
catch catch
catch y uso práctico try {
Copy-Item @copyParams
}
catch [System.UnauthorizedAccessException] {
Write-Warning ('Permisos insuficientes: {0}' -f $_.Exception.Message)
}
catch [System.IO.IOException] {
Write-Warning ('Problema de E/S: {0}' -f $_.Exception.Message)
}
catch {
Write-Warning ('Error {0}: {1}' -f $_.Exception.GetType().Name, $_.Exception.Message)
}
En PowerShell es común incluir un genérico
porque muchos cmdlets encapsulan las excepciones .NET dentro de objetos
catch catch , cuyas categorías o tipos varían entre
proveedores. Esto dificulta predecir el tipo exacto, por lo que un bloque genérico
suele ser una red de seguridad útil. Aun así, cuando anticipes escenarios típicos
(permisos, E/S), añade [ErrorRecord] [ErrorRecord] tipados para mensajes más
claros.
catch catch
En este apunte usaremos principalmente genéricos
para simplificar los ejemplos, aunque en contextos reales conviene combinar ambos
tipos según el grado de control y legibilidad que busques.
catch catch
Verificar y ejecutar comandos externos con seguridad
Patrón simple: resolver el binario real con ,
ejecutarlo con el call operator Get-Command Get-Command , y validar
& & . Devuelve un objeto con datos útiles para logs
o para controlar el flujo.
$LASTEXITCODE $LASTEXITCODE
#Requires -Version 7.5
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[string] $Name,
[Parameter(ValueFromRemainingArguments)]
[ValidateNotNull()]
[string[]] $Rest = @()
)
Set-StrictMode -Version 3.0
$cmds = Get-Command -Name $Name -CommandType Application -ErrorAction Stop
if ($cmds.Count -gt 1) {
Write-Warning ("Multiple executables named '{0}' were found. Using: {1}" -f
$Name, $cmds[0].Source)
}
$path = $cmds[0].Source
$originalEncoding = [Console]::OutputEncoding
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$out = & $path @Rest 2>&1
}
finally {
[Console]::OutputEncoding = $originalEncoding
}
if ($LASTEXITCODE -eq 0) {
[PSCustomObject]@{
ToolPath = $path
ExitCode = $LASTEXITCODE
Output = $out
}
}
else {
$nl = [Environment]::NewLine
$msg = ('{0} {1} returned exit code {2}.{3}Output:{3}{4}' -f $Name, ($Rest -join ' '),
$LASTEXITCODE, $nl, ($out -join $nl))
throw [System.Exception]::new($msg)
} ¿Qué acabamos de hacer?
-
: todo lo que quede después deValueFromRemainingArgumentsValueFromRemainingArgumentsse captura en-Name-Namesin intentar interpretarlo como parámetros del script. Así puedes pasar flags arbitrarios al binario:$Rest$Rest.Invoke-Tool -Name git -C . status --porcelainInvoke-Tool -Name git -C . status --porcelain -
: resuelve el ejecutable real (solo de tipo Application) a partir de todos los candidatos que encuentra (devuelve una colección) usando la heurística simple de quedarnos con el primer resultado. ConGet-Command ... -ErrorAction StopGet-Command ... -ErrorAction Stopconvertimos «no encontrado» en error terminante. El objeto devuelto es-ErrorAction Stop-ErrorAction Stop.[System.Management.Automation.CommandInfo][System.Management.Automation.CommandInfo] -
: ruta absoluta al binario resuelto (p. ej.,[CommandInfo].Source[CommandInfo].SourceC:\Program Files\Git\cmd\git.exe). Se usa para invocarlo de forma inequívoca. -
: establece UTF-8 temporalmente para capturar stdout/stderr con caracteres especiales. Se restaura en[Console]::OutputEncoding[Console]::OutputEncodingpara no afectar la sesión.finallyfinally - Línea clave:
:$out = & $path @Rest 2>&1$out = & $path @Rest 2>&1- call operator
ejecuta el binario.&& - Splatting
pasa los argumentos tal cual (array → args).@Rest@Rest -
: combina los flujos de salida estándar (stdout) y error estándar (stderr) en uno solo. En PowerShell (y en sistemas heredados de Unix), los números representan los streams:2>&12>&1-
→ salida estándar (stdout), lo que normalmente imprime el programa.11 -
→ salida de error (stderr), donde van los mensajes de error.22 -
redirige la salida hacia otro destino, y>>indica que ese destino es otro flujo.&&
significa literalmente «redirige el flujo 2 (errores) al mismo lugar que el flujo 1 (salida estándar)» , lo que permite capturar todo (mensajes y errores) en la misma variable2>&12>&1.$out$out -
-
-
: código de salida del proceso nativo.$LASTEXITCODE$LASTEXITCODE= éxito; cualquier otro se trata como error y se eleva con un mensaje detallado.00 -
: promueve el fallo a error terminante con contexto (comando, argumentos, código y salida), ideal para CI/CD y manejo en niveles superiores conthrow [System.Exception]::new($msg)throw [System.Exception]::new($msg)/trytry.catchcatch
Arreglos en PowerShell
En PowerShell, un arreglo ( ) es una
colección ordenada de elementos. Declaras un arreglo vacío con [T[]] [T[]] o simplemente listando valores separados por comas:
@() @()
# Arreglo vacío
$empty = @()
# Arreglo con valores
$tools = @('git', 'curl', 'wget')
# O simplemente:
$tools = 'git', 'curl', 'wget'
# Acceso por índice (base cero)
$tools[0] # 'git'
$tools[1] # 'curl'
# Contar elementos
$tools.Count # 3
En , el parámetro Invoke-Tool.ps1 Invoke-Tool.ps1 es un arreglo de strings ($Rest $Rest ) que captura
todos los argumentos restantes gracias a [string[]] [string[]] . Luego, ValueFromRemainingArguments ValueFromRemainingArguments (splatting) expande ese arreglo como
argumentos individuales al comando nativo.
@Rest @Rest
Streams de salida en PowerShell
PowerShell no se limita a imprimir texto: maneja múltiples streams de salida, lo que permite separar mensajes de éxito, errores, advertencias, depuración, etc. Esto hace posible redirigirlos o tratarlos de forma independiente en un mismo script.
De momento solo nos interesa la idea general: el stream 1 se usa
para la salida normal, y el resto (2–6) sirven para errores, advertencias, mensajes
verbosos, depuración, etc. Más adelante veremos con detalle cómo usar
para propagar errores no terminantes en
el pipeline; por ahora nos apoyaremos sobre todo en
salida estructurada y dejaremos la responsabilidad de imprimir en
pantalla a quien consuma estos scripts (otros cmdlets, pipelines o
herramientas de automatización), que decidirán qué mostrar, cuándo y cómo.
Write-Error Write-Error
| Stream | Descripción | Write Cmdlet |
| 1 | Éxito (Output) | |
| 2 | Error | |
| 3 | Warning | |
| 4 | Verbose | |
| 5 | Debug | |
| 6 | Information | |
| n/a | Progress | |
Ejercicio:
Fallback por comando
Requisitos
- Implementa
que intente ejecutar una tarea usando una cadena de alternativas de binarios. No interrogues el sistema operativo; en su lugar, resuelve comandos (conInvoke-WebRequestByFallback.ps1Invoke-WebRequestByFallback.ps1) y usaInvoke-Tool.ps1Invoke-Tool.ps1/trytrypara avanzar al siguiente.catchcatch - Escenario propuesto (puedes elegir otro): descargar un archivo probando primero
wgety luegocurl. - El script debe:
- Recibir
(obligatorio) y-Uri-Uri(obligatorio).-OutFile-OutFile - Probar
wgetdentro de/trytryconcatchcatch.-ErrorAction Stop-ErrorAction Stop - Si
wgetfalla, capturar el error y probarcurlen el bloque(anidando otrocatchcatch).trytry - Si ambos fallan,
con un mensaje consolidado.throwthrow - Devuelve un objeto estructurado con el estado de la operación.
-
Notas
wget wget
wget wget
Descargador común en Linux/BSD; puede instalarse en macOS/Windows. Usa
para definir el archivo de salida.
-O -O
wget # Descargar a un archivo específico
wget -O "<OutFile>" "<Uri>"
# Ejemplo
wget -O "./download/file.txt" "https://example.com/file.txt" curl curl
curl curl
Cliente de transferencia que viene preinstalado en macOS y muchas distros Linux;
en Windows 10+ suele estar disponible. Úsalo con
para seguir redirecciones y
-L -L para especificar archivo de salida.
-o -o
curl curl # Descargar siguiendo redirecciones
curl -L -o "<OutFile>" "<Uri>"
# Ejemplo
curl -L -o "./download/file.txt" "https://example.com/file.txt" Uso esperado
$params = @{
Uri = 'https://www.toptal.com/developers/gitignore/api/powershell,windows,linux,macos,visualstudiocode'
OutFile = '.gitignore'
Verbose = $true
}
./Invoke-WebRequestByFallback.ps1 @params Hints
- Usa
para obtener la ruta real deJoin-Path ... -ResolveJoin-Path ... -ResolveInvoke-Tool.ps1y fallar temprano si no existe.Invocador seguro$invoker = Join-Path $PSScriptRoot '..' 'tools' 'Invoke-Tool.ps1' -Resolve # Ejemplo de uso con call operator (&) y argumentos: & $invoker wget -O './out.txt' 'https://example.com/file.txt'valida la ruta en tiempo de inicio; evita fallos tardíos.-Resolve-Resolve - Prefiere tipar
y validar el esquema en lugar de[uri] $Uri[uri] $Uri:[string][string]Validación de URL robusta[Parameter(Mandatory)] [ValidateScript({ $_.Scheme -in @('http','https') })] [uri] $UriEl tipoparsea la dirección; el[uri][uri]asegura[ValidateScript][ValidateScript]http/https.
Solución
#Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory)]
[ValidateScript({ $_.Scheme -in @('http','https') })]
[uri] $Uri,
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[string] $OutFile
)
Set-StrictMode -Version 3.0
$invoker = Join-Path $PSScriptRoot '..' 'tools' 'Invoke-Tool.ps1' -Resolve
if ($PSCmdlet.ShouldProcess("$Uri to $OutFile", 'Download file')) {
try {
& $invoker wget -O $OutFile $Uri
[PSCustomObject]@{
Uri = $Uri
OutFile = $OutFile
Tool = 'wget'
Status = 'Success'
}
}
catch {
Write-Warning ('wget failed: {0}' -f $_.Exception.Message)
try {
& $invoker curl -L -o $OutFile $Uri
[PSCustomObject]@{
Uri = $Uri
OutFile = $OutFile
Tool = 'curl'
Status = 'Success'
}
}
catch {
Write-Warning ('curl failed: {0}' -f $_.Exception.Message)
throw [System.Exception]::new("Both wget and curl failed to download $Uri")
}
}
}Conclusiones
El hilo conductor de la lección es controlar el flujo a través de los errores.
Cuando la integridad importa, eleva fallos con
y manéjalos con
-ErrorAction Stop -ErrorAction Stop /try try /catch catch ; cuando buscas reportar sin interrumpir, prefiere errores no terminantes. Al ejecutar
binarios externos, resuélvelos primero (finally finally ),
ejecútalos con Get-Command Get-Command y valida con
& & o un wrapper como
$LASTEXITCODE $LASTEXITCODE , devolviendo salida estructurada
en lugar de texto plano.
Invoke-Tool.ps1 Invoke-Tool.ps1
Puntos clave
- Promueve errores críticos con
para activar-ErrorAction Stop-ErrorAction Stopdonde puedas decidir cómo reaccionar.catchcatch - En
,catchcatches el$_$_(tipo y mensaje en[ErrorRecord][ErrorRecord]).$_.Exception$_.Exception - Resuelve binarios con
y llama al path real.Get-Command -ErrorAction StopGet-Command -ErrorAction Stop - Ejecuta con
y valida con&&(o tu wrapper).$LASTEXITCODE$LASTEXITCODE -
mezcla stderr en stdout para capturar todo en una variable cuando te convenga simplificar el flujo.2>&12>&1
¿Qué nos llevamos?
El buen manejo de errores es diseño, no accidente. En esta lección vimos tres
ideas clave: elevar fallos no terminantes con para capturarlos con -ErrorAction Stop -ErrorAction Stop /try try cuando la integridad está en juego; envolver comandos externos (por ejemplo
con catch catch Invoke-Tool.ps1) para resolver el ejecutable real, validar
y devolver salida estructurada; y diseñar
scripts alrededor de capacidades (qué herramientas hay disponibles) en lugar de
suposiciones frágiles del entorno. Si registras con intención, priorizas la salida
estructurada sobre el texto suelto y «fail loudly» cuando
corresponde, tus scripts serán más confiables, portables y fáciles de mantener.
$LASTEXITCODE $LASTEXITCODE
¿Con ganas de más?
Referencias recomendadas
- “Handling errors” (pp. 279–286) en Learn PowerShell in a Month of Lunches, fourth edition: Covers Windows, Linux, and MacOS por Travis PlunkCapítulo práctico sobre manejo de errores en PowerShell que explica la diferencia entre errores no terminantes y excepciones, cómo forzar excepciones con
para poder capturarlas con-ErrorAction Stop-ErrorAction Stop/trytry/catchcatch, y el papel definallyfinally,$Error$Error(incluyendo el uso de-ErrorVariable-ErrorVariablepara acumular) y++. Advierte contra malas prácticas como silenciar errores globalmente, y muestra cuándo registrar, continuar o detener según el objetivo del script. Incluye patrones para comandos nativos y métodos .NET (cuándo ajustar temporalmente las preferencias), y ejemplos de$ErrorActionPreference$ErrorActionPreferencetipados. Ideal para entender qué registrar, cómo reaccionar ante fallos y cómo diseñar herramientas que fallen de forma explícita y depurable.catchcatch
Referencias adicionales
- “ about_Redirection ” en la documentación oficial de PowerShellBreve guía de redirección en PowerShell: explica cómo enviar salida a archivos y combinar flujos (éxito, error, warning, etc.) usando
,Out-FileOut-Filey operadores de redirección (Tee-ObjectTee-Object,>>,>>>>,n>&1n>&1). Incluye tabla de flujos redireccionables, cambios en PowerShell 7.4 para redirigir stdout de comandos nativos (conservando bytes), ejemplos prácticos (fusionar*>*>, redirigir todos los flujos, suprimir Information/Host), efectos de Action Preferences, consideraciones de codificación y ancho de salida, y notas sobre confusiones comunes con2>&12>&1vs. comparadores (>>,-gt-gt).-lt-lt - “ about_Try_Catch_Finally ” en la documentación oficial de PowerShellDescripción formal de cómo usar
/trytry/catchcatchpara manejar errores terminantes en PowerShell. Presenta la sintaxis general de los bloques, muestra cómo envolver código que puede fallar, cómo disponer de varios bloquesfinallyfinally(genéricos y tipados) y en qué orden se evalúan, y explica qué ocurre con las excepciones no capturadas. Detalla la relación concatchcatchy con las preferencias de error, cómo acceder a la información de la excepción víatraptrap/$_$_y las propiedades de$PSItem$PSItem([ErrorRecord][ErrorRecord]CategoryInfo,TargetObject, etc.), y cómo usarpara liberar recursos o dejar el sistema en un estado consistente. Incluye ejemplos prácticos y notas sobre buenas prácticas para escribir bloquesfinallyfinallyclaros y seguros.catchcatch