Pide tu presupuesto ya!

Manejo maestro de errores de PowerShell: una guía sensata

¿Estás cansado de ver esos molestos mensajes de error rojos en tus scripts de PowerShell? Si bien pueden parecer intimidantes, el manejo adecuado de los errores es esencial para crear una automatización PowerShell confiable. En este tutorial, aprenderá cómo implementar un manejo sólido de errores en sus scripts, desde comprender los tipos de errores hasta dominar los bloques try/catch.

Requisitos previos

Este tutorial asume que tienes:

  • Windows PowerShell 5.1 o PowerShell 7+ instalado
  • Familiaridad básica con las secuencias de comandos de PowerShell.
  • ¡La voluntad de aceptar los errores como oportunidades de aprendizaje!

Comprender los tipos de errores de PowerShell

Antes de profundizar en el manejo de errores, debe comprender los dos tipos principales de errores que puede generar PowerShell:

Errores de terminación

Estos son los errores más graves: errores que detienen por completo la ejecución del script. Encontrarás errores de terminación cuando:

  • Su secuencia de comandos tiene errores de sintaxis que le impiden analizarse
  • Se producen excepciones no controladas en llamadas a métodos .NET
  • Usted especifica explícitamente ErrorAction Stop
  • Los errores críticos de tiempo de ejecución hacen que sea imposible continuar

Errores que no terminan

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

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

El parámetro ErrorAction: su primera línea de defensa

Comencemos con un ejemplo práctico. Aquí hay una secuencia de comandos que intenta eliminar archivos que tengan más de una determinada cantidad 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)]."
}

Por defecto, Remove-Item genera errores no terminantes. Para que genere errores de terminación que podamos detectar, agregue -ErrorAction Stop:

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

Bloques Try/Catch: su error al manejar la navaja suiza

Ahora incluyamos la eliminación de nuestro archivo 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 de terminación) donde puedes:

  • Registra el error
  • Tomar medidas correctivas
  • Notificar a los administradores
  • Continuar la ejecución del script con gracia

Trabajar con $Error: su herramienta de investigación de errores

PowerShell mantiene una serie de objetos de error en el modo automático. $Error variable. pensar en $Error Como “grabador de caja negra” de PowerShell: realiza un seguimiento de cada error que ocurre durante la sesión de PowerShell, lo que lo hace invaluable para la resolución de problemas y la depuración.

A continuación le indicamos cuándo y por qué es posible que desee utilizar $Error:

  1. Solución de problemas de errores pasados: Incluso si no vio un mensaje de error rojo, $Error mantiene una historia:

    # View most recent error details
    $Error[0] | Format-List * -Force
    
    # Look at the last 5 errors
    $Error[0..4] | Select-Object CategoryInfo, Exception
    
    # Search for specific types of errors
    $Error | Where-Object { $_.Exception -is [System.UnauthorizedAccessException] }
    
  2. Guiones de depuración: Usar $Error para entender qué salió mal y dónde:

    # Get the exact line number and script where the error occurred
    $Error[0].InvocationInfo | Select-Object ScriptName, ScriptLineNumber, Line
    
    # See the full error call stack
    $Error[0].Exception.StackTrace
    
  3. Recuperación e informes de errores: Perfecto para crear informes de errores detallados:

    # Create an error report
    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 comprobar el estado del error:

    # Clear error history (useful at the start of scripts)
    $Error.Clear()
    
    # Count total errors (good for error threshold checks)
    if ($Error.Count -gt 10) {
        Write-Warning "High error count detected: $($Error.Count) errors"
    }
    

Ejemplo del mundo real que combina 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 (controlados por $MaximumErrorCount)
  • Es una matriz de tamaño fijo: los errores nuevos eliminan los antiguos cuando están llenos
  • Siempre revisa $Error[0] primero: es el error más reciente
  • Considere limpiar $Error al inicio de scripts importantes para un seguimiento limpio de errores

Múltiples bloques de captura: manejo de errores dirigido

Así como no usarías la misma herramienta para cada trabajo de reparación del hogar, no debes manejar todos los errores de PowerShell de la misma manera. Múltiples bloques catch le 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 de errores comunes que encontrará:

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

Aquí hay 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 de captura:

  1. El orden importa: anteponga excepciones más específicas
  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 errores en diferentes ubicaciones
  5. Utilice el tipo de excepción más específico posible
  6. Pruebe cada bloque de captura provocando deliberadamente cada tipo de error

Usando bloques Finalmente: limpia lo que ensucias

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

  • Cerrar identificadores de archivos
  • Desconectarse de las bases de datos
  • Liberar recursos del sistema
  • Restaurar la configuración original

He aquí 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"
    }
}

Finalmente, piense en la regla de un campista responsable: “Siempre limpie su campamento antes de partir, sin importar lo que sucedió durante el viaje”.

Mejores prácticas de manejo de errores

  1. Sea específico con las acciones de error
    En lugar de manta ErrorAction Stopúselo selectivamente en comandos donde necesite detectar errores.

  2. Usar variables de error

    Remove-Item $path -ErrorVariable removeError
    if ($removeError) {
        Write-Warning "Failed to remove item: $($removeError[0].Exception.Message)"
    }
    
  3. Registrar errores adecuadamente

    • Utilice advertencia de escritura para errores recuperables
    • Utilice Write-Error para problemas graves
    • Considere escribir en el registro de eventos de Windows en caso de fallas críticas
  4. Limpiar recursos
    Utilice siempre bloques finalmente para limpiar recursos como identificadores de archivos y conexiones de red.

  5. Manejo de errores de prueba
    Active errores deliberadamente para verificar que el manejo de errores funcione como se esperaba.

Poniéndolo todo junto

A continuación se muestra 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.
  • Utiliza bloques catch específicos para errores comunes
  • Registra tanto éxitos como fracasos.
  • Proporciona resultados detallados para la resolución de problemas.
  • Vuelve a generar errores críticos al script de llamada

Conclusión

El manejo adecuado de errores es crucial para que los scripts de PowerShell sean confiables. Al comprender los tipos de errores y utilizar los bloques try/catch de forma eficaz, puede crear scripts que manejen los fallos con elegancia y proporcionen comentarios significativos. Recuerde probar minuciosamente su manejo de errores: ¡su yo futuro se lo agradecerá cuando solucione problemas en producción!

¡Ahora adelante y detecta esos errores! Sólo recuerde: el único error grave es un error no controlado.

Written by

Leave a comment