Domina el Manejo de Errores en PowerShell: Una Guía Directa

¿Estás cansado de ver esos molestos mensajes de error rojos en tus scripts de PowerShell? Aunque puedan parecer intimidantes, el manejo adecuado de errores es esencial para construir automatizaciones de PowerShell confiables. En este tutorial, aprenderás cómo implementar un sólido manejo de errores en tus scripts, desde comprender los tipos de errores hasta dominar los bloques try/catch.

Prerrequisitos

Este tutorial asume que tienes:

  • Windows PowerShell 5.1 o PowerShell 7+ instalado
  • Una familiaridad básica con scripting de PowerShell
  • ¡Una disposición para abrazar los errores como oportunidades de aprendizaje!

Comprendiendo los Tipos de Errores en PowerShell

Antes de adentrarte en el manejo de errores, necesitas entender los dos tipos principales de errores que PowerShell puede arrojar:

Errores Terminantes

Estos son los más serios: errores que detienen completamente la ejecución del script. Te encontrarás con errores terminantes cuando:

  • Tu script tiene errores de sintaxis que impiden su análisis
  • Ocurren excepciones no controladas en llamadas a métodos de .NET
  • Especifiques explícitamente ErrorAction Stop
  • Errores críticos de tiempo de ejecución hacen imposible continuar

Errores No Terminantes

Estos son errores operativos más comunes que no detendrán tu script:

  • Errores de archivo no encontrado
  • Escenarios de permiso denegado
  • Problemas de conectividad de red
  • Valores de parámetros inválidos

El Parámetro ErrorAction: Tu Primera Línea de Defensa

Comencemos con un ejemplo práctico. Aquí hay un script que intenta eliminar archivos más antiguos que un cierto número de días:

param (
    [Parameter(Mandatory)]
    [string]$FolderPath,

    [Parameter(Mandatory)]
    [int]$DaysOld
)

$Now = Get-Date
$LastWrite = $Now.AddDays(-$DaysOld)
$oldFiles = (Get-ChildItem -Path $FolderPath -File -Recurse).Where{$_.LastWriteTime -le $LastWrite}

foreach ($file in $oldFiles) {
    Remove-Item -Path $file.FullName
    Write-Verbose -Message "Successfully removed [$($file.FullName)]."
}

De forma predeterminada, Remove-Item genera errores no terminantes. Para que genere errores terminantes que podamos capturar, añade -ErrorAction Stop:

Remove-Item -Path $file.FullName -ErrorAction Stop

Bloques Try/Catch: Su navaja suiza de manejo de errores

Ahora envolvamos nuestra eliminación de archivos en un bloque try/catch:

foreach ($file in $oldFiles) {
    try {
        Remove-Item -Path $file.FullName -ErrorAction Stop
        Write-Verbose -Message "Successfully removed [$($file.FullName)]."
    }
    catch {
        Write-Warning "Failed to remove file: $($file.FullName)"
        Write-Warning "Error: $($_.Exception.Message)"
    }
}

El bloque try contiene código que podría generar un error. Si ocurre un error, la ejecución salta al bloque catch (si es un error terminante) donde puedes:

  • Registrar el error
  • Tomar medidas correctivas
  • Notificar a los administradores
  • Continuar la ejecución del script de forma elegante

Trabajando con $Error: Su herramienta de investigación de errores

PowerShell mantiene una matriz de objetos de error en la variable automática $Error. Piensa en $Error como el “registrador de caja negra” de PowerShell: lleva un registro de cada error que ocurre durante tu sesión de PowerShell, haciéndolo invaluable para la resolución de problemas y depuración.

Aquí es cuándo y por qué podrías querer usar $Error:

  1. Resolución de Errores Pasados: Incluso si te perdiste ver un mensaje de error en rojo, $Error mantiene un historial:

    # Ver detalles del error más reciente
    $Error[0] | Format-List * -Force
    
    # Ver los últimos 5 errores
    $Error[0..4] | Select-Object CategoryInfo, Exception
    
    # Buscar tipos específicos de errores
    $Error | Where-Object { $_.Exception -is [System.UnauthorizedAccessException] }
    
  2. Depuración de scripts: Usa $Error para entender qué salió mal y dónde:

    # Obtener el número exacto de línea y script donde ocurrió el error
    $Error[0].InvocationInfo | Select-Object ScriptName, ScriptLineNumber, Line
    
    # Ver la pila de llamadas completa del error
    $Error[0].Exception.StackTrace
    
  3. Recuperación e informes de errores: Perfecto para crear informes detallados de errores:

    # Crear un informe de error
    function Write-ErrorReport {
        param($ErrorRecord = $Error[0])
    [PSCustomObject]@{
        TimeStamp = Get-Date
        ErrorMessage = $ErrorRecord.Exception.Message
        ErrorType = $ErrorRecord.Exception.GetType().Name
        Command = $ErrorRecord.InvocationInfo.MyCommand
        ScriptLine = $ErrorRecord.InvocationInfo.Line
        ErrorLineNumber = $ErrorRecord.InvocationInfo.ScriptLineNumber
        StackTrace = $ErrorRecord.ScriptStackTrace
    }
    

    }

  4. Gestión de sesiones: Limpiar errores o verificar el estado de errores:

    # Borrar el historial de errores (útil al inicio de scripts)
    $Error.Clear()
    
    # Contar errores totales (útil para comprobar umbrales de errores)
    if ($Error.Count -gt 10) {
        Write-Warning "Se detectó un alto recuento de errores: $($Error.Count) errores"
    }
    

Ejemplo del mundo real combinando estos conceptos:

function Test-DatabaseConnections {
    $Error.Clear()  # Start fresh

    try {
        # Attempt database operations...
    }
    catch {
        # If something fails, analyze recent errors
        $dbErrors = $Error | Where-Object {
            $_.Exception.Message -like "*SQL*" -or
            $_.Exception.Message -like "*connection*"
        }

        if ($dbErrors) {
            Write-ErrorReport $dbErrors[0] |
                Export-Csv -Path "C:\\Logs\\DatabaseErrors.csv" -Append
        }
    }
}

Consejos Profesionales:

  • $Error se mantiene por sesión de PowerShell
  • Tiene una capacidad predeterminada de 256 errores (controlada por $MaximumErrorCount)
  • Es un arreglo de tamaño fijo: los nuevos errores desplazan a los antiguos cuando está lleno
  • Siempre verifica $Error[0] primero: es el error más reciente
  • Considera limpiar $Error al inicio de scripts importantes para un seguimiento limpio de errores

Múltiples Bloques Catch: Manejo de Errores Específico

Así como no usarías la misma herramienta para cada trabajo de reparación en casa, no deberías manejar cada error de PowerShell de la misma manera. Múltiples bloques catch te permiten responder de manera diferente a diferentes tipos de errores.

Así es como funciona:

try {
    Remove-Item -Path $file.FullName -ErrorAction Stop
}
catch [System.UnauthorizedAccessException] {
    # This catches permission-related errors
    Write-Warning "Access denied to file: $($file.FullName)"
    Request-ElevatedPermissions -Path $file.FullName  # Custom function
}
catch [System.IO.IOException] {
    # This catches file-in-use errors
    Write-Warning "File in use: $($file.FullName)"
    Add-ToRetryQueue -Path $file.FullName  # Custom function
}
catch [System.Management.Automation.ItemNotFoundException] {
    # This catches file-not-found errors
    Write-Warning "File not found: $($file.FullName)"
    Update-FileInventory -RemovePath $file.FullName  # Custom function
}
catch {
    # This catches any other errors
    Write-Warning "Unexpected error: $_"
    Write-EventLog -LogName Application -Source "MyScript" -EntryType Error -EventId 1001 -Message $_
}

Tipos comunes de errores que encontrarás:

  • [System.UnauthorizedAccessException] – Permiso denegado
  • [System.IO.IOException] – Archivo bloqueado/en uso
  • [System.Management.Automation.ItemNotFoundException] – Archivo/ruta no encontrado
  • [System.ArgumentException] – Argumento inválido
  • [System.Net.WebException] – Problemas de red/web

Aquí tienes un ejemplo del mundo real que pone esto en práctica:

function Remove-StaleFiles {
    [CmdletBinding()]
    param(
        [string]$Path,
        [int]$RetryCount = 3,
        [int]$RetryDelaySeconds = 30
    )

    $retryQueue = @()

    foreach ($file in (Get-ChildItem -Path $Path -File)) {
        $attempt = 0
        do {
            $attempt++
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose "Successfully removed $($file.FullName)"
                break  # Exit the retry loop on success
            }
            catch [System.UnauthorizedAccessException] {
                if ($attempt -eq $RetryCount) {
                    # Log to event log and notify admin
                    $message = "Permission denied after $RetryCount attempts: $($file.FullName)"
                    Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1001 -Message $message
                    Send-AdminNotification -Message $message  # Custom function
                }
                else {
                    # Request elevated permissions and retry
                    Request-ElevatedAccess -Path $file.FullName  # Custom function
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch [System.IO.IOException] {
                if ($attempt -eq $RetryCount) {
                    # Add to retry queue for later
                    $retryQueue += $file.FullName
                    Write-Warning "File locked, added to retry queue: $($file.FullName)"
                }
                else {
                    # Wait and retry
                    Write-Verbose "File in use, attempt $attempt of $RetryCount"
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch {
                # Unexpected error - log and move on
                $message = "Unexpected error with $($file.FullName): $_"
                Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1002 -Message $message
                break  # Exit retry loop for unexpected errors
            }
        } while ($attempt -lt $RetryCount)
    }

    # Return retry queue for further processing
    if ($retryQueue) {
        return $retryQueue
    }
}

Consejos Profesionales para Múltiples Bloques Catch:

  1. El orden importa: coloca excepciones más específicas primero
  2. Utilice funciones personalizadas para manejar cada tipo de error de manera consistente
  3. Considere la lógica de reintento para errores transitorios
  4. Registre diferentes tipos de error en ubicaciones distintas
  5. Utilice el tipo de excepción más específico posible
  6. Pruebe cada bloque catch causando deliberadamente cada tipo de error

Usar bloques Finally: Limpiar después de ti

El bloque finally es tu equipo de limpieza; siempre se ejecuta, haya un error o no. Esto lo hace perfecto para:

  • Cerrar manejadores de archivos
  • Desconectar de bases de datos
  • Liberar recursos del sistema
  • Restaurar configuraciones originales

Aquí tienes un ejemplo práctico:

try {
    $stream = [System.IO.File]::OpenRead($file.FullName)
    # Process file contents here...
}
catch {
    Write-Warning "Error processing file: $_"
}
finally {
    # This runs even if an error occurred
    if ($stream) {
        $stream.Dispose()
        Write-Verbose "File handle released"
    }
}

Considera el finally como una regla de un campista responsable: “Siempre limpia tu campamento antes de irte, sin importar lo que haya sucedido durante el viaje.”

Mejores Prácticas para el Manejo de Errores

  1. Ser Específico con las Acciones de Error
    En lugar de usar ErrorAction Stop de manera general, úsalo selectivamente en comandos donde necesitas capturar errores.

  2. Utilizar Variables de Error

    Remove-Item $path -ErrorVariable removeError
    if ($removeError) {
        Write-Warning "Error al eliminar el elemento: $($removeError[0].Exception.Message)"
    }
    
  3. Registrar errores de forma apropiada

    • Usa Write-Warning para errores recuperables
    • Usa Write-Error para problemas serios
    • Considera escribir en el Registro de eventos de Windows para fallos críticos
  4. Limpiar recursos
    Siempre usa bloques finally para limpiar recursos como manejadores de archivos y conexiones de red.

  5. Probar el manejo de errores
    Provoca errores deliberadamente para verificar que tu manejo de errores funcione como se espera.

Integrándolo todo

Aquí tienes un ejemplo completo que incorpora estas mejores prácticas:

function Remove-OldFiles {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$FolderPath,

        [Parameter(Mandatory)]
        [int]$DaysOld,

        [string]$LogPath = "C:\\Logs\\file-cleanup.log"
    )

    try {
        # Validate input
        if (-not (Test-Path -Path $FolderPath)) {
            throw "Folder path '$FolderPath' does not exist"
        }

        $Now = Get-Date
        $LastWrite = $Now.AddDays(-$DaysOld)

        # Find old files
        $oldFiles = Get-ChildItem -Path $FolderPath -File -Recurse |
                    Where-Object {$_.LastWriteTime -le $LastWrite}

        foreach ($file in $oldFiles) {
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose -Message "Successfully removed [$($file.FullName)]"

                # Log success
                "$(Get-Date) - Removed file: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.UnauthorizedAccessException] {
                Write-Warning "Access denied to file: $($file.FullName)"
                "$ErrorActionPreference - Access denied: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.IO.IOException] {
                Write-Warning "File in use: $($file.FullName)"
                "$(Get-Date) - File in use: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch {
                Write-Warning "Unexpected error removing file: $_"
                "$(Get-Date) - Error: $_ - File: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
        }
    }
    catch {
        Write-Error "Critical error in Remove-OldFiles: $_"
        "$(Get-Date) - Critical Error: $_" |
            Add-Content -Path $LogPath
        throw  # Re-throw error to calling script
    }
}

Esta implementación:

  • Valida los parámetros de entrada
  • Usa bloques catch específicos para errores comunes
  • Registra tanto éxitos como fallos
  • Proporciona una salida detallada para solución de problemas
  • Vuelve a lanzar errores críticos al script que lo llama

Conclusión

El manejo adecuado de errores es crucial para scripts de PowerShell confiables. Al comprender los tipos de errores y utilizar bloques try/catch de manera efectiva, puedes crear scripts que manejen fallos con elegancia y proporcionen retroalimentación significativa. Recuerda probar tu manejo de errores a fondo; ¡tu yo del futuro te lo agradecerá cuando resuelvas problemas en producción!

¡Ahora avanza y atrapa esos errores! Solo recuerda: el único error malo es un error no manejado.

Source:
https://adamtheautomator.com/powershell-error-handling/