Domine o Tratamento de Erros do PowerShell: Um Guia Sem Rodeios

Você está cansado de ver aquelas mensagens de erro vermelhas incômodas em seus scripts do PowerShell? Embora possam parecer intimidadoras, o tratamento adequado de erros é essencial para construir uma automação confiável no PowerShell. Neste tutorial, você aprenderá como implementar um tratamento de erros robusto em seus scripts – desde compreender os tipos de erro até dominar os blocos try/catch.

Pré-requisitos

Este tutorial assume que você possui:

  • Windows PowerShell 5.1 ou PowerShell 7+ instalado
  • Conhecimento básico sobre scripts do PowerShell
  • Disposição para encarar erros como oportunidades de aprendizado!

Compreendendo os Tipos de Erro do PowerShell

Antes de mergulhar no tratamento de erros, você precisa entender os dois principais tipos de erros que o PowerShell pode gerar:

Erros Terminais

Esses são os mais sérios – erros que interrompem completamente a execução do script. Você encontrará erros terminais quando:

  • Seu script tiver erros de sintaxe que impeçam sua análise
  • Exceções não tratadas ocorrerem em chamadas de método .NET
  • Você especificar explicitamente ErrorAction Stop
  • Erros críticos em tempo de execução tornarem impossível continuar

Erros Não Terminais

Esses são erros operacionais mais comuns que não interrompem seu script:

  • Erros de arquivo não encontrado
  • Cenários de permissão negada
  • Problemas de conectividade de rede
  • Valores de parâmetros inválidos

O Parâmetro ErrorAction: Sua Primeira Linha de Defesa

Vamos começar com um exemplo prático. Aqui está um script que tenta remover arquivos mais antigos que um certo número de dias:

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 padrão, Remove-Item gera erros não terminais. Para fazê-lo gerar erros terminais que podemos capturar, adicione -ErrorAction Stop:

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

Blocos Try/Catch: Sua Canivete Suíço de Tratamento de Erros

Agora vamos encapsular nossa remoção de arquivos em um bloco 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)"
    }
}

O bloco try contém código que pode gerar um erro. Se um erro ocorrer, a execução salta para o bloco catch (se for um erro terminal) onde você pode:

  • Registrar o erro
  • Tomar ações corretivas
  • Notificar administradores
  • Continuar a execução do script de forma tranquila

Trabalhando com $Error: Sua Ferramenta de Investigação de Erros

O PowerShell mantém um array de objetos de erro na variável automática $Error. Pense em $Error como o “gravador de caixa preta” do PowerShell – ele rastreia todos os erros que ocorrem durante sua sessão do PowerShell, tornando-se inestimável para solução de problemas e depuração.

Aqui está quando e por que você pode querer usar $Error:

  1. Solução de Problemas de Erros Anteriores: Mesmo que você tenha perdido uma mensagem de erro vermelha, $Error mantém um histórico:

    # Veja os detalhes do erro mais recente
    $Error[0] | Format-List * -Force
    
    # Veja os últimos 5 erros
    $Error[0..4] | Select-Object CategoryInfo, Exception
    
    # Pesquise por tipos específicos de erros
    $Error | Where-Object { $_.Exception -is [System.UnauthorizedAccessException] }
    
  2. Depuração de Scripts: Use $Error para entender o que deu errado e onde:

    # Obtenha o número exato da linha e o script onde o erro ocorreu
    $Error[0].InvocationInfo | Select-Object ScriptName, ScriptLineNumber, Line
    
    # Veja a pilha de chamadas de erro completa
    $Error[0].Exception.StackTrace
    
  3. Recuperação e Relatório de Erros: Perfeito para criar relatórios de erro detalhados:

    # Crie um relatório de erro
    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. Gerenciamento de Sessão: Limpe os erros ou verifique o status de erro:

    # Limpe o histórico de erros (útil no início de scripts)
    $Error.Clear()
    
    # Contar erros totais (bom para verificações de limite de erro)
    if ($Error.Count -gt 10) {
        Write-Warning "Alto número de erros detectado: $($Error.Count) erros"
    }
    

Exemplo do mundo real combinando esses conceitos:

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
        }
    }
}

Dicas profissionais:

  • $Error é mantido por sessão PowerShell
  • Possui uma capacidade padrão de 256 erros (controlado por $MaximumErrorCount)
  • É um array de tamanho fixo – novos erros substituem os antigos quando está cheio
  • Sempre verifique $Error[0] primeiro – é o erro mais recente
  • Considere limpar $Error no início de scripts importantes para um rastreamento de erro limpo

Múltiplos Blocos Catch: Manipulação de Erro Direcionada

Assim como você não usaria a mesma ferramenta para cada trabalho de reparo em casa, você não deve tratar cada erro PowerShell da mesma maneira. Múltiplos blocos catch permitem que você responda de maneira diferente a diferentes tipos de erros.

Aqui está 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 comuns de erro que você encontrará:

  • [System.UnauthorizedAccessException] – Permissão negada
  • [System.IO.IOException] – Arquivo bloqueado/em uso
  • [System.Management.Automation.ItemNotFoundException] – Arquivo/caminho não encontrado
  • [System.ArgumentException] – Argumento inválido
  • [System.Net.WebException] – Problemas de rede/web

Aqui está um exemplo do mundo real que coloca isso em prática:

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
    }
}

Dicas profissionais para Múltiplos Blocos Catch:

  1. A ordem importa – coloque exceções mais específicas primeiro
  2. Use funções personalizadas para lidar com cada tipo de erro de forma consistente
  3. Considere lógica de repetição para erros transitórios
  4. Registre diferentes tipos de erro em diferentes locais
  5. Use o tipo de exceção mais específico possível
  6. Teste cada bloco catch causando deliberadamente cada tipo de erro

Usando Blocos Finally: Limpe Após Você Mesmo

O bloco finally é sua equipe de limpeza – ele sempre é executado, quer haja um erro ou não. Isso o torna perfeito para:

  • Fechar manipuladores de arquivos
  • Desconectar de bancos de dados
  • Liberar recursos do sistema
  • Restaurar configurações originais

Aqui está um exemplo prático:

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"
    }
}

Pense no finally como uma regra de um campista responsável: “Sempre limpe seu acampamento antes de partir, não importa o que aconteceu durante a viagem.”

Melhores Práticas de Tratamento de Erros

  1. Seja Específico com Ações de Erro
    Em vez de ErrorAction Stop genérico, use-o seletivamente em comandos onde você precisa capturar erros.

  2. Use Variáveis de Erro

    Remove-Item $path -ErrorVariable removeError
    if ($removeError) {
        Write-Warning "Falha ao remover item: $($removeError[0].Exception.Message)"
    }
    
  3. Registre Erros Apropriadamente

    • Use Write-Warning para erros recuperáveis
    • Use Write-Error para problemas sérios
    • Considere registrar no Log de Eventos do Windows para falhas críticas
  4. Limpeza de Recursos
    Sempre use blocos finally para limpar recursos como manipuladores de arquivos e conexões de rede.

  5. Teste o Tratamento de Erros
    Acione deliberadamente erros para verificar se o tratamento de erros funciona como esperado.

Colocando Tudo Junto

Aqui está um exemplo completo que incorpora essas melhores práticas:

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 implementação:

  • Valida parâmetros de entrada
  • Usa blocos catch específicos para erros comuns
  • Registra tanto sucessos quanto falhas
  • Fornece saída detalhada para solução de problemas
  • Relança erros críticos para o script chamador

Conclusão

O tratamento adequado de erros é crucial para scripts PowerShell confiáveis. Ao entender os tipos de erro e usar blocos try/catch de forma eficaz, você pode construir scripts que lidam com falhas de maneira elegante e fornecem feedback significativo. Lembre-se de testar seu tratamento de erros minuciosamente – seu eu do futuro agradecerá quando estiver solucionando problemas em produção!

Agora siga em frente e capture esses erros! Apenas lembre-se – o único erro ruim é um erro não tratado.

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