Master PowerShell Error Handling: A No-Nonsense Guide

Published:24 December 2024 - 6 min. read

Are you tired of seeing those pesky red error messages in your PowerShell scripts? While they may look intimidating, proper error handling is essential for building reliable PowerShell automation. In this tutorial, you’ll learn how to implement robust error handling in your scripts – from understanding error types to mastering try/catch blocks.

Prerequisites

This tutorial assumes you have:

  • Windows PowerShell 5.1 or PowerShell 7+ installed
  • Basic familiarity with PowerShell scripting
  • A willingness to embrace errors as learning opportunities!

Understanding PowerShell Error Types

Before diving into handling errors, you need to understand the two main types of errors PowerShell can throw:

Terminating Errors

These are the serious ones – errors that completely halt script execution. You’ll encounter terminating errors when:

  • Your script has syntax errors preventing it from parsing
  • Unhandled exceptions occur in .NET method calls
  • You explicitly specify ErrorAction Stop
  • Critical runtime errors make it impossible to continue

Non-Terminating Errors

These are more common operational errors that won’t stop your script:

  • File not found errors
  • Permission denied scenarios
  • Network connectivity issues
  • Invalid parameter values

The ErrorAction Parameter: Your First Line of Defense

Let’s start with a practical example. Here’s a script that tries to remove files older than a certain number of days:

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)]."
}

By default, Remove-Item generates non-terminating errors. To make it generate terminating errors that we can catch, add -ErrorAction Stop:

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

Try/Catch Blocks: Your Error Handling Swiss Army Knife

Now let’s wrap our file removal in a try/catch block:

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

The try block contains code that might generate an error. If an error occurs, execution jumps to the catch block (if it’s a terminating error) where you can:

  • Log the error
  • Take corrective action
  • Notify administrators
  • Continue script execution gracefully

Working with $Error: Your Error Investigation Tool

PowerShell maintains an array of error objects in the automatic $Error variable. Think of $Error as PowerShell’s “black box recorder” – it keeps track of every error that occurs during your PowerShell session, making it invaluable for troubleshooting and debugging.

Here’s when and why you might want to use $Error:

  1. Troubleshooting Past Errors: Even if you missed seeing a red error message, $Error maintains a history:

    # 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. Debugging Scripts: Use $Error to understand what went wrong and where:

    # 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. Error Recovery and Reporting: Perfect for creating detailed error reports:

    # 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. Session Management: Clean up errors or check error status:

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

Real-world example combining these concepts:

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

Pro Tips:

  • $Error is maintained per PowerShell session
  • It has a default capacity of 256 errors (controlled by $MaximumErrorCount)
  • It’s a fixed-size array – new errors push out old ones when full
  • Always check $Error[0] first – it’s the most recent error
  • Consider clearing $Error at the start of important scripts for clean error tracking

Multiple Catch Blocks: Targeted Error Handling

Just like you wouldn’t use the same tool for every home repair job, you shouldn’t handle every PowerShell error the same way. Multiple catch blocks let you respond differently to different types of errors.

Here’s how it works:

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

Common error types you’ll encounter:

  • [System.UnauthorizedAccessException] – Permission denied
  • [System.IO.IOException] – File locked/in use
  • [System.Management.Automation.ItemNotFoundException] – File/path not found
  • [System.ArgumentException] – Invalid argument
  • [System.Net.WebException] – Network/web issues

Here’s a real-world example that puts this into practice:

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

Pro Tips for Multiple Catch Blocks:

  1. Order matters – put more specific exceptions first
  2. Use custom functions to handle each error type consistently
  3. Consider retry logic for transient errors
  4. Log different error types to different locations
  5. Use the most specific exception type possible
  6. Test each catch block by deliberately causing each error type

Using Finally Blocks: Clean Up After Yourself

The finally block is your cleanup crew – it always executes, whether there’s an error or not. This makes it perfect for:

  • Closing file handles
  • Disconnecting from databases
  • Releasing system resources
  • Restoring original settings

Here’s a practical example:

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

Think of finally like a responsible camper’s rule: “Always clean up your campsite before leaving, no matter what happened during the trip.”

Error Handling Best Practices

  1. Be Specific with Error Actions
    Instead of blanket ErrorAction Stop, use it selectively on commands where you need to catch errors.

  2. Use Error Variables

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

    • Use Write-Warning for recoverable errors
    • Use Write-Error for serious issues
    • Consider writing to the Windows Event Log for critical failures
  4. Clean Up Resources
    Always use finally blocks to clean up resources like file handles and network connections.

  5. Test Error Handling
    Deliberately trigger errors to verify your error handling works as expected.

Putting It All Together

Here’s a complete example incorporating these best practices:

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

This implementation:

  • Validates input parameters
  • Uses specific catch blocks for common errors
  • Logs both successes and failures
  • Provides verbose output for troubleshooting
  • Re-throws critical errors to the calling script

Conclusion

Proper error handling is crucial for reliable PowerShell scripts. By understanding error types and using try/catch blocks effectively, you can build scripts that gracefully handle failures and provide meaningful feedback. Remember to test your error handling thoroughly – your future self will thank you when troubleshooting issues in production!

Now go forth and catch those errors! Just remember – the only bad error is an unhandled error.

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!