Error Handling

Summary: in this tutorial, you will learn handle errors gracefully in powershell. master try/catch/finally, terminating vs non-terminating errors, erroraction, $error, throw, trap, custom error records, and defensive coding patterns.

Error Handling

Error handling separates fragile scripts from production-ready automation. Without proper error handling, scripts fail silently, corrupt data, leave resources in inconsistent states, and provide no diagnostic information when things go wrong.

PowerShell's error handling system is sophisticated, built on .NET exceptions, but adds PowerShell-specific concepts that can initially confuse developers from other languages — particularly the distinction between terminating and non-terminating errors.

This tutorial teaches you to write resilient scripts that gracefully handle failures, provide useful diagnostic information, and recover when possible.

Why Error Handling Matters

Consider a script that processes files:

# Without error handling
Get-ChildItem *.txt | ForEach-Object {
    $content = Get-Content $_.FullName
    $data = $content | ConvertFrom-Json
    Save-ProcessedData $data
}
 

What happens when:

  • A file is locked by another process?
  • A file contains invalid JSON?
  • The disk runs out of space during save?
  • Network connection fails during save?

Without error handling, the script either stops completely (terminating error) or continues with corrupted state (non-terminating error), potentially causing cascading failures. Neither is acceptable for production code.

With proper error handling, you can:

  • Detect failures as they occur
  • Diagnose what went wrong (which file, what error, when)
  • Decide whether to retry, skip, or stop
  • Log errors for later analysis
  • Clean up resources properly
  • Report meaningful status to users or monitoring systems

Understanding PowerShell's Two Error Types

PowerShell distinguishes between two fundamentally different error types, and this distinction is crucial to proper error handling.

Terminating Errors: Stop Everything

Terminating errors immediately halt execution and cannot be ignored:

  • Syntax errors (caught at parse time)
  • Explicit throw statements
  • .NET exceptions (division by zero, null reference, etc.)
  • Cmdlet errors when using -ErrorAction Stop
# Terminating error — execution stops here
throw "Critical failure occurred"
Write-Host "This never executes"
 
# Also terminating — .NET exception
$result = 1 / 0    # DivideByZeroException
Write-Host "This never executes either"
 

When a terminating error occurs, PowerShell searches up the call stack for a catch block. If none is found, the script/command terminates.

Non-Terminating Errors: Report and Continue

Non-terminating errors report the problem but continue processing:

  • Most cmdlet errors by default
  • File not found, access denied, network timeouts
  • Invalid parameters (when validation allows continuation)
# Non-terminating errors — continues to next item
Get-Item "missing1.txt", "missing2.txt", "missing3.txt"
# Error displayed for missing1.txt
# Error displayed for missing2.txt
# Error displayed for missing3.txt
# Script continues after all errors displayed
 
Write-Host "Script continues executing"    # This DOES run
 

Why does PowerShell do this? Pipeline processing. When you process thousands of files, you don't want one missing file to stop the entire operation. PowerShell reports each error but continues with remaining items.

Critical concept: try/catch only catches terminating errors by default

This is PowerShell's #1 source of confusion for error handling:

try {
    Get-Item "missing.txt"    # Non-terminating error
    Write-Host "This still runs!"    # ✓ Executes
}
catch {
    Write-Host "Never reached"       # ✗ Never executes
}
 

The catch block doesn't execute because the error wasn't terminating. To catch non-terminating errors, you must use -ErrorAction Stop:

try {
    Get-Item "missing.txt" -ErrorAction Stop    # Now terminating
    Write-Host "This doesn't run"
}
catch {
    Write-Host "Error caught!"    # ✓ Executes
}
 

Try / Catch / Finally: Structured Error Handling

The try/catch/finally structure is PowerShell's primary error handling mechanism, inherited from C# and similar to many modern languages.

Basic Try/Catch

try {
    # Code that might fail
    $content = Get-Content "config.json" -ErrorAction Stop
    $config = $content | ConvertFrom-Json
    Write-Host "Loaded configuration successfully"
}
catch {
    # Handle any error that occurs in try block
    Write-Warning "Failed to load configuration: $($_.Exception.Message)"
    # Set default configuration
    $config = @{ Port = 8080; LogLevel = "Info" }
}
 

How it works:

  1. PowerShell executes code in the try block
  2. If a terminating error occurs, execution immediately jumps to catch
  3. The catch block handles the error (logging, cleanup, fallback logic, etc.)
  4. Execution continues after the catch block

The Finally Block: Guaranteed Cleanup

finally always executes, whether an error occurred or not:

$file = $null
try {
    $file = [System.IO.File]::Open("data.bin", [System.IO.FileMode]::Open)
    # Process file...
    $bytes = New-Object byte[] 1024
    $file.Read($bytes, 0, 1024)
}
catch {
    Write-Error "Failed to read file: $($_.Exception.Message)"
}
finally {
    # ALWAYS executes — even if error occurred
    if ($null -ne $file) {
        $file.Close()
        Write-Verbose "File closed"
    }
}
 

finally is essential for resource cleanup:

  • Closing files, database connections, network connections
  • Releasing locks
  • Restoring system state
  • Deallocating memory

Rule: Use finally for cleanup that must happen regardless of success or failure.

Catching Specific Exception Types

PowerShell lets you catch different exception types with specific handlers:

try {
    $data = Get-Content "data.json" | ConvertFrom-Json
    $result = 100 / $data.Divisor
}
catch [System.IO.FileNotFoundException] {
    # Specific handler for missing file
    Write-Warning "Configuration file not found — using defaults"
    $data = @{ Divisor = 10 }
}
catch [System.DivideByZeroException] {
    # Specific handler for division by zero
    Write-Error "Invalid configuration: Divisor cannot be zero"
    return
}
catch [System.ArgumentException] {
    # Specific handler for invalid JSON
    Write-Error "Configuration file contains invalid JSON"
    return
}
catch {
    # Catch-all for any other error
    Write-Error "Unexpected error occurred: $($_.Exception.GetType().FullName)"
    Write-Error $_.Exception.Message
    throw    # Re-throw if you can't handle it
}
 

Why catch specific types?

  • Different errors require different responses
  • Missing file → use defaults
  • Invalid data → stop processing
  • Network timeout → retry
  • Access denied → escalate to admin

Specific handlers make your error recovery logic precise and maintainable.

The Error Record ($_): Rich Error Information

Inside a catch block, $_ is an ErrorRecord object containing extensive information about the error:

try {
    Get-Content "C:\Windows\System32\protected.txt" -ErrorAction Stop
}
catch {
    Write-Host "Error Details:" -ForegroundColor Red
    Write-Host "  Message: $($_.Exception.Message)"
    Write-Host "  Type: $($_.Exception.GetType().FullName)"
    Write-Host "  Target: $($_.TargetObject)"
    Write-Host "  Category: $($_.CategoryInfo.Category)"
    Write-Host "  Line: $($_.InvocationInfo.ScriptLineNumber)"
    Write-Host "  Script: $($_.InvocationInfo.ScriptName)"
 
    # Full stack trace
    Write-Host "  Stack Trace:"
    Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
}
 

Key ErrorRecord properties:

  • Exception.Message: Human-readable error description
  • Exception.GetType(): Specific exception type for type-specific handling
  • TargetObject: The object that caused the error
  • CategoryInfo: Error category (PermissionDenied, ObjectNotFound, etc.)
  • InvocationInfo: Where the error occurred (script name, line number)
  • ScriptStackTrace: Call stack showing execution path to error

This information is invaluable for debugging and logging.

Nested Try/Catch for Granular Control

You can nest try/catch blocks for fine-grained error handling:

try {
    Write-Host "Starting deployment..." -ForegroundColor Cyan
 
    # Step 1: Backup
    try {
        Write-Host "Creating backup..."
        Backup-Data -Path "C:\AppData" -Destination "C:\Backup"
    }
    catch {
        Write-Warning "Backup failed: $($_.Exception.Message)"
        # Decide: continue anyway or abort?
        throw "Cannot proceed without backup"
    }
 
    # Step 2: Deploy
    try {
        Write-Host "Deploying application..."
        Deploy-Application -Path "C:\Deploy"
    }
    catch {
        Write-Error "Deployment failed: $($_.Exception.Message)"
        # Rollback
        Write-Host "Rolling back..." -ForegroundColor Yellow
        Restore-Backup -Path "C:\Backup"
        throw
    }
 
    Write-Host "Deployment successful!" -ForegroundColor Green
}
catch {
    Write-Error "Deployment process failed: $($_.Exception.Message)"
    Send-AlertEmail -Subject "Deployment Failed" -Body $_.Exception.Message
}
 

Nested try/catch lets you handle errors at the appropriate granularity while still having an outer handler for catastrophic failures.

ErrorAction Parameter: Controlling Error Behavior

Every cmdlet with [CmdletBinding()] supports the -ErrorAction parameter, which controls how errors are handled.

ErrorAction Values

# Stop: Make error terminating (catchable)
Get-Item "missing.txt" -ErrorAction Stop
 
# Continue: Display error, continue execution (default for most cmdlets)
Get-Item "missing.txt" -ErrorAction Continue
 
# SilentlyContinue: Suppress error display, continue execution
Get-Item "missing.txt" -ErrorAction SilentlyContinue
 
# Ignore: Completely ignore error (don't even log to $Error)
Get-Item "missing.txt" -ErrorAction Ignore
 
# Inquire: Prompt user how to proceed
Get-Item "missing.txt" -ErrorAction Inquire
 
# Suspend: Suspend workflow (workflows only)
 

Common patterns:

Convert non-terminating to terminating:

try {
    $files = Get-ChildItem "C:\Data" -ErrorAction Stop
}
catch {
    Write-Error "Failed to access data directory"
    return
}
 

Silent failure with testing:

$exists = Test-Path "important-file.txt" -ErrorAction SilentlyContinue
if ($exists) {
    # File exists, proceed
} else {
    # File missing, use fallback
}
 

Suppress expected errors:

# Testing connectivity — errors expected
$servers | ForEach-Object {
    $online = Test-Connection $_ -Count 1 -Quiet -ErrorAction SilentlyContinue
    [PSCustomObject]@{
        Server = $_
        Online = $online
    }
}
 

ErrorActionPreference: Session Default

$ErrorActionPreference sets the default -ErrorAction for the session:

# Check current preference
$ErrorActionPreference    # Default: Continue
 
# Make all errors terminating in this script
$ErrorActionPreference = 'Stop'
try {
    Get-Item "missing.txt"    # Now catchable without -ErrorAction
}
catch {
    Write-Host "Caught error"
}
 
# Restore default
$ErrorActionPreference = 'Continue'
 

When to change $ErrorActionPreference:

  • In scripts where any error should stop execution (strict mode)
  • During development/testing to catch all errors
  • In CI/CD pipelines where failures must not be silently ignored

Caution: Changing $ErrorActionPreference affects all subsequent commands. Scope it carefully or restore the original value.

The $Error Variable: Error History

PowerShell maintains an automatic variable $Error containing all errors from the session:

# $Error is an ArrayList of ErrorRecords
$Error.Count    # Number of errors
$Error[0]       # Most recent error
$Error[-1]      # Oldest error
 
# Inspect recent error
$Error[0].Exception.Message
$Error[0].InvocationInfo
 
# Clear error history
$Error.Clear()
 

Practical uses:

Check if last command succeeded:

Invoke-WebRequest "https://api.example.com/data" -ErrorAction SilentlyContinue
if ($Error.Count -gt 0 -and $Error[0].Exception -is [System.Net.WebException]) {
    Write-Warning "Network request failed"
}
 

Collect errors from batch operation:

$Error.Clear()
$files | ForEach-Object {
    Copy-Item $_ "C:\Backup" -ErrorAction SilentlyContinue
}
if ($Error.Count -gt 0) {
    Write-Warning "Encountered $($Error.Count) errors during backup"
    $Error | ForEach-Object {
        Write-Host "  Error: $($_.Exception.Message)" -ForegroundColor Red
    }
}
 

ErrorVariable: Custom Error Collection

Instead of using $Error, you can collect errors in a custom variable:

# -ErrorVariable captures errors to named variable
Get-ChildItem "C:\Windows" -Recurse -ErrorVariable myErrors -ErrorAction SilentlyContinue
 
# Analyze collected errors
$myErrors.Count
$myErrors | Group-Object -Property CategoryInfo.Category
 

The custom error variable doesn't include the $ prefix when specified:

Get-Item "missing.txt" -ErrorVariable myErr -ErrorAction SilentlyContinue
$myErr[0].Exception.Message    # Access with $ prefix
 

Use -ErrorVariable when:

  • Processing multiple items and want to analyze failures afterward
  • Need to separate errors from different operations
  • Building diagnostic reports

Throw: Creating Custom Errors

throw creates a terminating error:

function Divide-Numbers {
    param(
        [double]$Numerator,
        [double]$Denominator
    )
 
    if ($Denominator -eq 0) {
        throw "Cannot divide by zero"
    }
 
    return $Numerator / $Denominator
}
 
try {
    $result = Divide-Numbers 10 0
}
catch {
    Write-Error "Division failed: $($_.Exception.Message)"
}
 

Throw custom exception objects:

function Get-UserData {
    param([string]$UserId)
 
    if ($UserId -notmatch '^\d{6}$') {
        $ex = New-Object System.ArgumentException(
            "User ID must be exactly 6 digits",
            "UserId"
        )
        throw $ex
    }
 
    # Fetch user data...
}
 
try {
    Get-UserData "ABC123"
}
catch [System.ArgumentException] {
    Write-Error "Invalid user ID format: $($_.Exception.Message)"
}
 

Write-Error vs Throw

Write-Error creates a non-terminating error, throw creates a terminating error:

function Test-Difference {
    Write-Host "Before Write-Error"
    Write-Error "This is a non-terminating error"
    Write-Host "After Write-Error — this runs"
 
    Write-Host "Before throw"
    throw "This is a terminating error"
    Write-Host "After throw — this NEVER runs"
}
 

When to use each:

  • throw: Fatal errors that prevent continuation (invalid state, missing dependencies, critical failures)
  • Write-Error: Non-fatal issues to report while continuing (validation warnings, missing optional data, retryable failures)

Trap: Legacy Error Handling

trap is PowerShell's legacy error handling mechanism, predating try/catch:

trap {
    Write-Warning "An error occurred: $($_.Exception.Message)"
    continue    # or: break
}
 
Get-Item "missing.txt"
Write-Host "Script continues"
 

trap behaviors:

  • continue: Handle error, continue execution
  • break: Handle error, stop execution

Modern recommendation: Prefer try/catch over trap. It's more explicit, better structured, and familiar to developers from other languages. Use trap only when maintaining legacy scripts.

Practical Error Handling Patterns

Pattern 1: Graceful Fallback

Attempt primary method, fall back to alternative on failure:

function Get-Configuration {
    [CmdletBinding()]
    param()
 
    # Try loading from primary location
    try {
        Write-Verbose "Loading configuration from primary location..."
        $config = Get-Content "C:\ProgramData\MyApp\config.json" -ErrorAction Stop | ConvertFrom-Json
        Write-Verbose "Configuration loaded successfully"
        return $config
    }
    catch [System.IO.FileNotFoundException] {
        Write-Warning "Primary configuration not found, trying secondary location..."
    }
    catch {
        Write-Warning "Failed to load primary configuration: $($_.Exception.Message)"
    }
 
    # Try loading from secondary location
    try {
        Write-Verbose "Loading configuration from secondary location..."
        $config = Get-Content "$env:USERPROFILE\MyApp\config.json" -ErrorAction Stop | ConvertFrom-Json
        Write-Verbose "Configuration loaded from secondary location"
        return $config
    }
    catch {
        Write-Warning "Failed to load secondary configuration: $($_.Exception.Message)"
    }
 
    # Both failed — use defaults
    Write-Warning "Using default configuration"
    return @{
        Port = 8080
        LogLevel = "Info"
        EnableMetrics = $true
    }
}
 

Pattern 2: Retry with Exponential Backoff

Retry transient failures with increasing delays:

function Invoke-WithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [scriptblock]$ScriptBlock,
 
        [int]$MaxRetries = 3,
        [int]$InitialDelaySeconds = 1
    )
 
    $attempt = 0
    $delay = $InitialDelaySeconds
 
    while ($attempt -lt $MaxRetries) {
        $attempt++
 
        try {
            Write-Verbose "Attempt $attempt of $MaxRetries..."
            $result = & $ScriptBlock
            Write-Verbose "Operation succeeded on attempt $attempt"
            return $result
        }
        catch {
            $lastError = $_
 
            if ($attempt -lt $MaxRetries) {
                Write-Warning "Attempt $attempt failed: $($_.Exception.Message)"
                Write-Host "Retrying in $delay seconds..." -ForegroundColor Yellow
                Start-Sleep -Seconds $delay
                $delay *= 2    # Exponential backoff: 1, 2, 4, 8...
            }
        }
    }
 
    # All retries exhausted
    Write-Error "Operation failed after $MaxRetries attempts: $($lastError.Exception.Message)"
    throw $lastError
}
 
# Usage
$data = Invoke-WithRetry -MaxRetries 5 -ScriptBlock {
    Invoke-RestMethod "https://api.example.com/data" -TimeoutSec 10
}
 

Pattern 3: Transaction-Style (All or Nothing)

Either all operations succeed, or all are rolled back:

function Deploy-Application {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]$SourcePath,
        [string]$DestinationPath
    )
 
    $backupPath = "${DestinationPath}.backup"
    $deployed = $false
 
    try {
        # Step 1: Backup existing
        if ($PSCmdlet.ShouldProcess($DestinationPath, "Backup existing installation")) {
            Write-Host "Creating backup..." -ForegroundColor Cyan
            if (Test-Path $DestinationPath) {
                Move-Item $DestinationPath $backupPath -Force -ErrorAction Stop
                Write-Verbose "Backup created at $backupPath"
            }
        }
 
        # Step 2: Deploy new version
        if ($PSCmdlet.ShouldProcess($SourcePath, "Deploy new version")) {
            Write-Host "Deploying application..." -ForegroundColor Cyan
            Copy-Item $SourcePath $DestinationPath -Recurse -Force -ErrorAction Stop
            $deployed = $true
            Write-Verbose "Application deployed successfully"
        }
 
        # Step 3: Verify deployment
        Write-Host "Verifying deployment..." -ForegroundColor Cyan
        $configFile = Join-Path $DestinationPath "app.config"
        if (-not (Test-Path $configFile)) {
            throw "Deployment verification failed: app.config not found"
        }
 
        # Step 4: Cleanup backup
        if (Test-Path $backupPath) {
            Remove-Item $backupPath -Recurse -Force
            Write-Verbose "Backup removed"
        }
 
        Write-Host "Deployment successful!" -ForegroundColor Green
        return $true
    }
    catch {
        Write-Error "Deployment failed: $($_.Exception.Message)"
 
        # Rollback if we deployed but verification failed
        if ($deployed) {
            Write-Host "Rolling back deployment..." -ForegroundColor Yellow
            try {
                Remove-Item $DestinationPath -Recurse -Force -ErrorAction Stop
                if (Test-Path $backupPath) {
                    Move-Item $backupPath $DestinationPath -Force
                    Write-Host "Rollback successful" -ForegroundColor Green
                }
            }
            catch {
                Write-Error "CRITICAL: Rollback failed: $($_.Exception.Message)"
                Write-Error "Manual intervention required!"
            }
        }
 
        return $false
    }
}
 

Pattern 4: Comprehensive Error Logging

Log errors with context for troubleshooting:

function Write-ErrorLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord,
 
        [string]$LogPath = "C:\Logs\errors.log",
 
        [hashtable]$Context = @{}
    )
 
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 
    $logEntry = @"
========================================
Timestamp: $timestamp
Error: $($ErrorRecord.Exception.Message)
Type: $($ErrorRecord.Exception.GetType().FullName)
Category: $($ErrorRecord.CategoryInfo.Category)
Target: $($ErrorRecord.TargetObject)
Script: $($ErrorRecord.InvocationInfo.ScriptName)
Line: $($ErrorRecord.InvocationInfo.ScriptLineNumber)
Command: $($ErrorRecord.InvocationInfo.Line.Trim())
 
Context:
$($Context.GetEnumerator() | ForEach-Object { "  $($_.Key): $($_.Value)" } | Out-String)
 
Stack Trace:
$($ErrorRecord.ScriptStackTrace)
========================================
 
"@
 
    Add-Content -Path $LogPath -Value $logEntry
    Write-Verbose "Error logged to $LogPath"
}
 
# Usage
try {
    Process-ImportantData -Source "data.json"
}
catch {
    Write-ErrorLog -ErrorRecord $_ -Context @{
        User = $env:USERNAME
        Computer = $env:COMPUTERNAME
        WorkingDirectory = Get-Location
    }
    throw
}
 

Exercises

🏋️ Exercise 1: Robust File Processor

Create a function that processes multiple files with comprehensive error handling:

  • Try to read each file
  • Handle file-not-found separately from access-denied
  • Log errors but continue processing remaining files
  • Return summary of successes and failures
Show Solution
function Process-Files {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$FilePaths
    )
 
    begin {
        $results = @{
            Success = @()
            NotFound = @()
            AccessDenied = @()
            OtherErrors = @()
        }
    }
 
    process {
        foreach ($filePath in $FilePaths) {
            Write-Verbose "Processing $filePath..."
 
            try {
                $content = Get-Content $filePath -ErrorAction Stop
                $lineCount = $content.Count
 
                $results.Success += [PSCustomObject]@{
                    FilePath = $filePath
                    Lines = $lineCount
                }
 
                Write-Host "✓ $filePath ($lineCount lines)" -ForegroundColor Green
            }
            catch [System.IO.FileNotFoundException] {
                $results.NotFound += $filePath
                Write-Warning "✗ File not found: $filePath"
            }
            catch [System.UnauthorizedAccessException] {
                $results.AccessDenied += $filePath
                Write-Warning "✗ Access denied: $filePath"
            }
            catch {
                $results.OtherErrors += [PSCustomObject]@{
                    FilePath = $filePath
                    Error = $_.Exception.Message
                }
                Write-Warning "✗ Error processing $filePath: $($_.Exception.Message)"
            }
        }
    }
 
    end {
        Write-Host ""
        Write-Host "=== Processing Summary ===" -ForegroundColor Cyan
        Write-Host "Successful: $($results.Success.Count)" -ForegroundColor Green
        Write-Host "Not found: $($results.NotFound.Count)" -ForegroundColor Yellow
        Write-Host "Access denied: $($results.AccessDenied.Count)" -ForegroundColor Yellow
        Write-Host "Other errors: $($results.OtherErrors.Count)" -ForegroundColor Red
 
        return $results
    }
}
 
# Test
$testFiles = @(
    "C:\Windows\System.ini",    # Should succeed
    "C:\missing.txt",           # File not found
    "C:\Windows\System32\config\SAM",    # Access denied
    "C:\Windows\win.ini"        # Should succeed
)
 
$results = Process-Files -FilePaths $testFiles -Verbose
 
🏋️ Exercise 2: API Call with Retry

Implement a function that calls an API with automatic retry on network errors:

  • Maximum 3 retries
  • Exponential backoff (1s, 2s, 4s)
  • Different handling for 404 vs 500 vs network errors
  • Return data on success, null on permanent failure
Show Solution
function Invoke-APIWithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Uri,
 
        [int]$MaxRetries = 3,
        [int]$InitialDelaySeconds = 1
    )
 
    $attempt = 0
    $delay = $InitialDelaySeconds
 
    while ($attempt -lt $MaxRetries) {
        $attempt++
        Write-Verbose "Attempt $attempt of $MaxRetries for $Uri"
 
        try {
            $response = Invoke-RestMethod -Uri $Uri -ErrorAction Stop -TimeoutSec 10
            Write-Host "✓ API call succeeded on attempt $attempt" -ForegroundColor Green
            return $response
        }
        catch {
            $statusCode = $_.Exception.Response.StatusCode.value__
 
            if ($statusCode -eq 404) {
                # Resource not found — don't retry
                Write-Error "Resource not found (404): $Uri"
                return $null
            }
            elseif ($statusCode -ge 500) {
                # Server error — retry
                Write-Warning "Server error ($statusCode) on attempt $attempt"
            }
            elseif ($_.Exception -is [System.Net.WebException]) {
                # Network error — retry
                Write-Warning "Network error on attempt $attempt: $($_.Exception.Message)"
            }
            else {
                # Other error — don't retry
                Write-Error "Unrecoverable error: $($_.Exception.Message)"
                return $null
            }
 
            if ($attempt -lt $MaxRetries) {
                Write-Host "Retrying in $delay seconds..." -ForegroundColor Yellow
                Start-Sleep -Seconds $delay
                $delay *= 2
            }
        }
    }
 
    Write-Error "API call failed after $MaxRetries attempts"
    return $null
}
 
# Test
$data = Invoke-APIWithRetry -Uri "https://api.github.com/users/powershell" -Verbose
if ($data) {
    Write-Host "Retrieved data for: $($data.name)"
}
 

Summary

Robust error handling transforms scripts from fragile prototypes to production-ready automation:

  • Two error types: Terminating (catchable) vs non-terminating (continue)
  • try/catch/finally: Structured exception handling with guaranteed cleanup
  • Specific exception handlers: Different errors require different responses
  • ErrorRecord ($_): Rich error information for diagnosis and logging
  • -ErrorAction: Control whether errors terminate or continue
  • throw: Create custom terminating errors
  • Practical patterns: Fallback, retry, transaction, comprehensive logging

Master error handling to write scripts that fail gracefully, provide useful diagnostics, and recover when possible.

Was this page helpful?
SR

Written by the ShellRAG Team

The ShellRAG editorial team writes practical, beginner-friendly PowerShell tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.

Learn more about us →