Error handling is a crucial aspect of writing robust PowerShell scripts. When things go wrong—and they will—proper error handling helps your scripts gracefully manage problems, provide meaningful feedback, and continue operating when possible. In this guide, we’ll explore PowerShell’s error handling mechanisms and learn how to write more resilient scripts.

Understanding PowerShell Errors

PowerShell has two main types of errors:

  1. Terminating Errors: These stop script execution completely
  2. Non-Terminating Errors: These generate an error message but allow the script to continue

Understanding this distinction is key to effective error handling.

Types of Errors in PowerShell

Let’s see some examples of different error types:

Non-terminating error example

Write-Output "This will show an error but continue…"
Get-Item "C:\NonExistentFile.txt" -ErrorAction SilentlyContinue
Write-Output "Script continues after non-terminating error"
# You can make non-terminating errors into terminating errors
try {
    Get-Item "C:\NonExistentFile.txt" -ErrorAction Stop
    Write-Output "This won't be reached"
}
catch {
    Write-Output "Caught the error: $($_.Exception.Message)"
}

Terminating error example

try {
    # This will cause a terminating error
    $result = 1 / 0
    Write-Output "This won't be reached"
}
catch {
    Write-Output "Caught terminating error: $($_.Exception.Message)"
}

Write-Output "Script continues after try-catch block"

The Try-Catch-Finally Structure

The try-catch-finally structure is the primary mechanism for handling errors in PowerShell:

  • Try: Contains code that might generate an error
  • Catch: Handles the error if one occurs
  • Finally: Runs regardless of whether an error occurred (optional)

Basic Try-Catch Example

Basic try-catch structure

try {
    # Code that might fail
    $file = Get-Content "C:\NonExistentFile.txt" -ErrorAction Stop
    Write-Output "File content: $file"
}
catch {
    # Handle the error
    Write-Output "Error occurred: $($_.Exception.Message)"
    Write-Output "Error type: $($_.Exception.GetType().Name)"
}

Write-Output "Script completed successfully"

Try-Catch-Finally Example

Try-catch-finally structure

$filePath = "C:\Temp\test.txt"

try {
    Write-Output "Attempting to read file: $filePath"

    # Create a test file first
    if (-not (Test-Path "C:\Temp")) {
        New-Item -ItemType Directory -Path "C:\Temp" -Force
    }
    "Sample content" | Out-File -FilePath $filePath

    $content = Get-Content $filePath -ErrorAction Stop
    Write-Output "File read successfully: $content"
}
catch [System.IO.FileNotFoundException] {
    Write-Output "File not found: $filePath"
}
catch [System.UnauthorizedAccessException] {
    Write-Output "Access denied to file: $filePath"
}
catch {
    Write-Output "Unexpected error: $($_.Exception.Message)"
}
finally {
    Write-Output "Cleanup: Removing test file if it exists"
    if (Test-Path $filePath) {
        Remove-Item $filePath -Force
    }
}

Multiple Catch Blocks for Specific Errors

You can handle different types of errors with specific catch blocks:

Function Test-SpecificErrorHandling {
    param([string]$Path)

    try {
        Write-Output "Attempting to access: $Path"

        # This could throw various types of errors
        $item = Get-Item $Path -ErrorAction Stop
        $content = Get-Content $Path -ErrorAction Stop

        Write-Output "Successfully read file with $($content.Count) lines"
    }
    catch [System.IO.FileNotFoundException] {
        Write-Output "File not found: $Path"
        Write-Output "Suggestion: Check if the file path is correct"
    }
    catch [System.IO.DirectoryNotFoundException] {
        Write-Output "Directory not found in path: $Path"
        Write-Output "Suggestion: Create the directory first"
    }
    catch [System.UnauthorizedAccessException] {
        Write-Output "Access denied: $Path"
        Write-Output "Suggestion: Run as administrator or check permissions"
    }
    catch [System.IO.IOException] {
        Write-Output "IO Error accessing: $Path"
        Write-Output "Suggestion: File might be locked by another process"
    }
    catch {
        Write-Output "Unexpected error: $($_.Exception.GetType().Name)"
        Write-Output "Error message: $($_.Exception.Message)"
    }
}

# Test with different scenarios
Test-SpecificErrorHandling -Path "C:\NonExistentFile.txt"
Test-SpecificErrorHandling -Path "C:\NonExistent\Directory\file.txt"

The $Error Variable and Error Information

PowerShell maintains an automatic variable called $Error that contains all errors from the current session:

# Clear previous errors
$Error.Clear()

# Generate some errors intentionally
Get-Item "C:\NonExistent1.txt" -ErrorAction SilentlyContinue
Get-Item "C:\NonExistent2.txt" -ErrorAction SilentlyContinue

# Examine the error collection
Write-Output "Total errors in session: $($Error.Count)"
Write-Output "Most recent error: $($Error[0].Exception.Message)"

# Loop through recent errors
for ($i = 0; $i -lt [Math]::Min(3, $Error.Count); $i++) {
    Write-Output "Error $($i + 1): $($Error[$i].Exception.Message)"
}

# Error object properties
if ($Error.Count -gt 0) {
    $recentError = $Error[0]
    Write-Output "`nError Details:"
    Write-Output "  Message: $($recentError.Exception.Message)"
    Write-Output "  Type: $($recentError.Exception.GetType().Name)"
    Write-Output "  Category: $($recentError.CategoryInfo.Category)"
    Write-Output "  Target: $($recentError.TargetObject)"
}

ErrorAction Parameter

The -ErrorAction parameter controls how cmdlets respond to errors:

  • Stop: Converts non-terminating errors to terminating errors
  • Continue: Default behavior, shows error and continues
  • SilentlyContinue: Suppresses error display but continues
  • Inquire: Prompts user for action
  • Ignore: Completely ignores errors (PowerShell 3.0+)
# Example of different ErrorAction values
$testFiles = @("C:\Windows\System32\notepad.exe", "C:\NonExistent.txt", "C:\Windows\System32\calc.exe")

Write-Output "=== ErrorAction: Continue (default) ==="
foreach ($file in $testFiles) {
    $item = Get-Item $file -ErrorAction Continue
    if ($item) { Write-Output "Found: $($item.Name)" }
}

Write-Output "`n=== ErrorAction: SilentlyContinue ==="
foreach ($file in $testFiles) {
    $item = Get-Item $file -ErrorAction SilentlyContinue
    if ($item) { Write-Output "Found: $($item.Name)" }
}

Write-Output "`n=== ErrorAction: Stop with Try-Catch ==="
foreach ($file in $testFiles) {
    try {
        $item = Get-Item $file -ErrorAction Stop
        Write-Output "Found: $($item.Name)"
    }
    catch {
        Write-Output "Not found: $file"
    }
}

ErrorVariable Parameter

The -ErrorVariable parameter allows you to capture errors in a custom variable:

# Using ErrorVariable to capture errors
$myErrors = @()

# Test multiple operations and collect errors
Get-Item "C:\Windows\System32\notepad.exe" -ErrorVariable +myErrors -ErrorAction SilentlyContinue
Get-Item "C:\NonExistent1.txt" -ErrorVariable +myErrors -ErrorAction SilentlyContinue
Get-Item "C:\NonExistent2.txt" -ErrorVariable +myErrors -ErrorAction SilentlyContinue

Write-Output "Captured $($myErrors.Count) errors:"
foreach ($error in $myErrors) {
    Write-Output "  - $($error.Exception.Message)"
}

# You can also use ErrorVariable without the + to overwrite instead of append
Get-Item "C:\AnotherNonExistent.txt" -ErrorVariable singleError -ErrorAction SilentlyContinue
if ($singleError) {
    Write-Output "`nSingle error captured: $($singleError.Exception.Message)"
}

Practical Error Handling Examples

Let’s look at some real-world scenarios where proper error handling is essential.

Example 1: Safe file operations

function Copy-FilesSafely {
    param(
        [string[]]$SourceFiles,
        [string]$DestinationPath
    )

    $results = @()

    # Ensure destination exists
    try {
        if (-not (Test-Path $DestinationPath)) {
            New-Item -ItemType Directory -Path $DestinationPath -Force -ErrorAction Stop
            Write-Output "Created destination directory: $DestinationPath"
        }
    }
    catch {
        Write-Output "Failed to create destination directory: $($_.Exception.Message)"
        return
    }

    foreach ($sourceFile in $SourceFiles) {
        try {
            # Check if source file exists
            if (-not (Test-Path $sourceFile)) {
                throw "Source file does not exist: $sourceFile"
            }

            $fileName = Split-Path $sourceFile -Leaf
            $destinationFile = Join-Path $DestinationPath $fileName

            Copy-Item -Path $sourceFile -Destination $destinationFile -ErrorAction Stop

            $results += [PSCustomObject]@{
                SourceFile = $sourceFile
                Status = "Success"
                Message = "Copied successfully"
            }

            Write-Output "Copied: $fileName"
        }
        catch {
            $results += [PSCustomObject]@{
                SourceFile = $sourceFile
                Status = "Failed"
                Message = $_.Exception.Message
            }

            Write-Output "Failed to copy $sourceFile`: $($_.Exception.Message)"
        }
    }

    return $results
}

# Test the function
$testFiles = @(
    "C:\Windows\System32\notepad.exe",
    "C:\NonExistent.txt",
    "C:\Windows\System32\calc.exe"
)

$results = Copy-FilesSafely -SourceFiles $testFiles -DestinationPath "C:\Temp\SafeCopy"
Write-Output "`nOperation Results:"
$results | Format-Table -AutoSize

Example 2: Database-like operations with rollback

function Invoke-BatchOperations {
    param([hashtable[]]$Operations)

    $completedOperations = @()
    $allSuccessful = $true

    try {
        foreach ($operation in $Operations) {
            try {
                Write-Output "Performing: $($operation.Description)"

                switch ($operation.Type) {
                    "CreateFile" {
                        New-Item -ItemType File -Path $operation.Path -Value $operation.Content -Force -ErrorAction Stop
                        $completedOperations += @{Type = "DeleteFile"; Path = $operation.Path}
                    }
                    "CreateDirectory" {
                        New-Item -ItemType Directory -Path $operation.Path -Force -ErrorAction Stop
                        $completedOperations += @{Type = "DeleteDirectory"; Path = $operation.Path}
                    }
                    "ModifyFile" {
                        # Backup original content first
                        $originalContent = Get-Content $operation.Path -ErrorAction Stop
                        Set-Content -Path $operation.Path -Value $operation.NewContent -ErrorAction Stop
                        $completedOperations += @{Type = "RestoreFile"; Path = $operation.Path; Content = $originalContent}
                    }
                    default {
                        throw "Unknown operation type: $($operation.Type)"
                    }
                }

                Write-Output "Completed: $($operation.Description)"
            }
            catch {
                Write-Output "Failed: $($operation.Description) - $($_.Exception.Message)"
                $allSuccessful = $false
                throw # Re-throw to trigger rollback
            }
        }

        Write-Output "All operations completed successfully!"
    }
    catch {
        Write-Output "Rolling back completed operations..."

        # Rollback in reverse order
        for ($i = $completedOperations.Count - 1; $i -ge 0; $i--) {
            $rollback = $completedOperations[$i]
            try {
                switch ($rollback.Type) {
                    "DeleteFile" {
                        Remove-Item -Path $rollback.Path -Force -ErrorAction Stop
                        Write-Output "Rolled back: Deleted file $($rollback.Path)"
                    }
                    "DeleteDirectory" {
                        Remove-Item -Path $rollback.Path -Recurse -Force -ErrorAction Stop
                        Write-Output "Rolled back: Deleted directory $($rollback.Path)"
                    }
                    "RestoreFile" {
                        Set-Content -Path $rollback.Path -Value $rollback.Content -ErrorAction Stop
                        Write-Output "Rolled back: Restored file $($rollback.Path)"
                    }
                }
            }
            catch {
                Write-Output "Rollback failed for: $($rollback.Path) - $($_.Exception.Message)"
            }
        }

        Write-Output "Batch operation failed and was rolled back"
    }
}

# Test batch operations
$operations = @(
    @{Type = "CreateDirectory"; Path = "C:\Temp\BatchTest"; Description = "Create test directory"},
    @{Type = "CreateFile"; Path = "C:\Temp\BatchTest\test1.txt"; Content = "Test content 1"; Description = "Create test file 1"},
    @{Type = "CreateFile"; Path = "C:\Temp\BatchTest\test2.txt"; Content = "Test content 2"; Description = "Create test file 2"},
    @{Type = "CreateFile"; Path = "C:\InvalidPath\test.txt"; Content = "This will fail"; Description = "Create file in invalid path (will trigger rollback)"}
)

Invoke-BatchOperations -Operations $operations

Example 3: Web request with retry logic

function Invoke-WebRequestWithRetry {
    param(
        [string]$Url,
        [int]$MaxRetries = 3,
        [int]$RetryDelaySeconds = 2
    )

    for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
        try {
            Write-Output "Attempt $attempt of $MaxRetries for: $Url"

            # Simulate web request (replace with actual Invoke-WebRequest in real scenarios)
            if ($Url -like "*badurl*") {
                throw "Simulated network error"
            }

            # Simulate successful response
            $response = [PSCustomObject]@{
                StatusCode = 200
                Content = "Simulated successful response from $Url"
                Url = $Url
            }

            Write-Output "Request successful on attempt $attempt"
            return $response
        }
        catch {
            Write-Output "Attempt $attempt failed: $($_.Exception.Message)"

            if ($attempt -eq $MaxRetries) {
                Write-Output "All $MaxRetries attempts failed for: $Url"
                throw "Failed to retrieve $Url after $MaxRetries attempts. Last error: $($_.Exception.Message)"
            }
            else {
                Write-Output "⏳ Waiting $RetryDelaySeconds seconds before retry..."
                Start-Sleep -Seconds $RetryDelaySeconds
            }
        }
    }
}

# Test retry logic
try {
    $response1 = Invoke-WebRequestWithRetry -Url "https://good-url.com"
    Write-Output "Response: $($response1.Content)"
}
catch {
    Write-Output "Final error: $($_.Exception.Message)"
}

Write-Output "`n" + "="*50

try {
    $response2 = Invoke-WebRequestWithRetry -Url "https://badurl.com" -MaxRetries 2
}
catch {
    Write-Output "Final error: $($_.Exception.Message)"
}

Best Practices for Error Handling

Be specific with Error Types

# Good: Specific error handling
function Read-ConfigFile {
    param([string]$Path)

    try {
        $config = Get-Content $Path -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        return $config
    }
    catch [System.IO.FileNotFoundException] {
        Write-Warning "Config file not found: $Path. Using default configuration."
        return @{DefaultSetting = $true}
    }
    catch [System.ArgumentException] {
        Write-Error "Invalid JSON in config file: $Path"
        throw
    }
    catch {
        Write-Error "Unexpected error reading config: $($_.Exception.Message)"
        throw
    }
}

# Test with non-existent file
$config = Read-ConfigFile -Path "C:\NonExistent\config.json"
Write-Output "Config loaded: $($config | ConvertTo-Json)"

Provide clear and meaningful error messages

function Connect-ToService {
    param(
        [string]$ServiceName,
        [string]$Server = "localhost"
    )

    try {
        # Simulate connection attempt
        if ($ServiceName -eq "BadService") {
            throw [System.InvalidOperationException]::new("Service not available")
        }

        Write-Output "Connected to $ServiceName on $Server"
        return $true
    }
    catch [System.InvalidOperationException] {
        $errorMsg = @"
Failed to connect to service '$ServiceName' on server '$Server'

Possible causes:
1. Service is not running
2. Service is not installed
3. Network connectivity issues
4. Insufficient permissions

Suggestions:
- Verify the service name is correct
- Check if the service is running: Get-Service -Name '$ServiceName'
- Test network connectivity to '$Server'
- Ensure you have proper permissions
"@
        Write-Error $errorMsg
        return $false
    }
    catch {
        Write-Error "Unexpected error connecting to $ServiceName`: $($_.Exception.Message)"
        return $false
    }
}

# Test the function
Connect-ToService -ServiceName "GoodService"
Connect-ToService -ServiceName "BadService"

Have an error log

Always have an error log so you can review what went wrong. You can create your own functions for that, but also use transcripts by leveraging Start-Transcript. Which ever you chose, is up to you.

Use the Finally block for cleanup

The Finally block is always executed, regardless of how the script runs.

function Process-FileWithCleanup {
    param([string]$FilePath)

    $tempFile = $null
    $fileHandle = $null

    try {
        # Create temporary working file
        $tempFile = [System.IO.Path]::GetTempFileName()
        Write-Output "Created temp file: $tempFile"

        # Simulate file processing
        if (-not (Test-Path $FilePath)) {
            throw "Source file not found: $FilePath"
        }

        Copy-Item -Path $FilePath -Destination $tempFile -ErrorAction Stop

        # Simulate some processing
        $content = Get-Content $tempFile
        $processedContent = $content | ForEach-Object { $_.ToUpper() }
        Set-Content -Path $tempFile -Value $processedContent

        Write-Output "File processed successfully"
        return $processedContent
    }
    catch {
        Write-Output "Error processing file: $($_.Exception.Message)"
        throw
    }
    finally {
        # Cleanup always happens
        if ($tempFile -and (Test-Path $tempFile)) {
            Remove-Item $tempFile -Force
            Write-Output "Cleaned up temp file: $tempFile"
        }

        if ($fileHandle) {
            $fileHandle.Close()
            Write-Output "Closed file handle"
        }
    }
}

# Test with existing file
try {
    $result = Process-FileWithCleanup -FilePath "C:\Windows\System32\drivers\etc\hosts"
    Write-Output "First few lines: $($result[0..2] -join ', ')"
}
catch {
    Write-Output "Process failed: $($_.Exception.Message)"
}

# Test with non-existent file
try {
    Process-FileWithCleanup -FilePath "C:\NonExistent.txt"
}
catch {
    Write-Output "Process failed as expected: $($_.Exception.Message)"
}

Common pifalls and how to avoid them

Catching too broadly

# Bad: Catches everything, might hide important errors
function Bad-ErrorHandling {
    try {
        # Some operation
        Get-Item "C:\test.txt" -ErrorAction Stop
    }
    catch {
        # This catches everything, even programming errors
        Write-Output "Something went wrong"
    }
}

# Good: Specific error handling
function Good-ErrorHandling {
    try {
        Get-Item "C:\test.txt" -ErrorAction Stop
    }
    catch [System.IO.FileNotFoundException] {
        Write-Output "File not found - this is expected and handled"
    }
    catch [System.UnauthorizedAccessException] {
        Write-Output "Access denied - check permissions"
    }
    catch {
        # Only catches truly unexpected errors
        Write-Error "Unexpected error: $($_.Exception.Message)"
        throw # Re-throw unexpected errors
    }
}

Good-ErrorHandling

incorrect use of the ErrorAction parameter

function Test-MultipleFiles {
    param([string[]]$FilePaths)

    $results = @()

    foreach ($path in $FilePaths) {
        # Use ErrorAction Stop to make errors catchable
        try {
            $file = Get-Item $path -ErrorAction Stop
            $results += [PSCustomObject]@{
                Path = $path
                Status = "Found"
                Size = $file.Length
            }
        }
        catch {
            $results += [PSCustomObject]@{
                Path = $path
                Status = "Not Found"
                Size = 0
            }
        }
    }

    return $results
}

$testPaths = @(
    "C:\Windows\System32\notepad.exe",
    "C:\NonExistent.txt",
    "C:\Windows\System32\calc.exe"
)

$results = Test-MultipleFiles -FilePaths $testPaths
$results | Format-Table -AutoSize

Conclusion

Proper error handling is essential for creating robust PowerShell scripts. By understanding the different types of errors and using try-catch-finally blocks effectively, you can create scripts that gracefully handle problems and provide meaningful feedback to users.

Key Takeaways:

1. Understand error types: Distinguish between terminating and non-terminating errors

2. Use try-catch-finally: Structure your error handling properly

3. Be specific: Catch specific error types when possible

4. Use ErrorAction: Control how cmdlets respond to errors

5. Provide meaningful messages: Help users understand what went wrong and how to fix it

6. Log appropriately: Keep records of errors for troubleshooting

7. Clean up resources: Use finally blocks to ensure cleanup happens

8. Test error scenarios: Validate that your error handling works as expected

Remember:

– Don’t catch errors too broadly unless you re-throw unexpected ones

– Use ErrorAction Stop to make non-terminating errors catchable

– Always clean up resources in finally blocks

– Provide actionable error messages

– Test both success and failure scenarios

Master these error handling techniques, and your PowerShell scripts will be much more reliable and user-friendly!

Leave a Reply

Your email address will not be published. Required fields are marked *