Skip to content

Error Handling (Try-Catch-Finally)

Note

Learn how to handle errors gracefully using try/catch/finally blocks and error handling best practices.

Overview

Error handling lets your scripts respond to problems instead of crashing. Without error handling, a single failure can stop your entire script. With proper error handling, you can catch errors, log them, retry operations, or gracefully exit while cleaning up resources.

Basic Syntax

try {
    # Code that might fail
    Get-Item "C:\DoesNotExist.txt"
}
catch {
    # Code that runs if an error occurs
    Write-Output "Error: $($_.Exception.Message)"
}
finally {
    # Code that ALWAYS runs (error or not)
    # Good for cleanup: closing files, connections, etc.
    Write-Output "Cleanup complete"
}

Key Points

  • try block contains code that might fail
  • catch block runs only if an error occurs in the try block
  • finally block ALWAYS runs, whether there was an error or not
  • Not all errors are catchable by default (see ErrorAction)
  • Use $_ or $PSItem in catch block to access error details

Error Types

Terminating vs Non-Terminating Errors

# NON-TERMINATING ERROR (by default)
# Shows error but continues running
Get-Item "C:\DoesNotExist.txt"
Write-Output "Script continues..."  # This still runs

# TERMINATING ERROR
# Stops execution - can be caught
try {
    Get-Item "C:\DoesNotExist.txt" -ErrorAction Stop  # Force terminating
    Write-Output "This won't run"
}
catch {
    Write-Output "Caught the error!"
}

Making Errors Catchable

# Won't catch - non-terminating error
try {
    Get-Item "C:\DoesNotExist.txt"  # Error shown but not caught
}
catch {
    Write-Output "This won't run"
}

# WILL catch - forced to terminating
try {
    Get-Item "C:\DoesNotExist.txt" -ErrorAction Stop
}
catch {
    Write-Output "Caught it!"  # This runs
}

# Alternative: Set preference for all commands
$ErrorActionPreference = "Stop"
try {
    Get-Item "C:\DoesNotExist.txt"  # Now catchable
}
catch {
    Write-Output "Caught it!"
}

Basic Error Handling

Simple Try-Catch

try {
    $content = Get-Content -Path "C:\config.txt" -ErrorAction Stop
    Write-Output "File loaded successfully"
}
catch {
    Write-Output "Failed to load file: $($_.Exception.Message)"
}

Try-Catch-Finally

$file = $null
try {
    $file = [System.IO.File]::Open("C:\data.txt", "Open")
    # Do something with file
    Write-Output "Processing file..."
}
catch {
    Write-Error "Failed to process file: $_"
}
finally {
    # Always clean up, even if error occurred
    if ($null -ne $file) {
        $file.Close()
        Write-Output "File closed"
    }
}

Accessing Error Information

Error Object Properties

try {
    Get-Item "C:\DoesNotExist.txt" -ErrorAction Stop
}
catch {
    # $_ or $PSItem contains the error

    Write-Output "Error Message: $($_.Exception.Message)"
    Write-Output "Error Type: $($_.Exception.GetType().FullName)"
    Write-Output "Failed Item: $($_.TargetObject)"
    Write-Output "Script Line: $($_.InvocationInfo.ScriptLineNumber)"
    Write-Output "Line Content: $($_.InvocationInfo.Line)"

    # Full error details
    Write-Output "`nFull Error:"
    Write-Output $_ | Format-List * -Force
}

Output Example:

Error Message: Cannot find path 'C:\DoesNotExist.txt'
Error Type: System.Management.Automation.ItemNotFoundException
Failed Item: C:\DoesNotExist.txt
Script Line: 2
Line Content:     Get-Item "C:\DoesNotExist.txt" -ErrorAction Stop

Catching Specific Errors

Catch Different Error Types

try {
    $number = [int]"NotANumber"  # Type conversion error
}
catch [System.InvalidCastException] {
    Write-Output "Can't convert to number"
}
catch [System.IO.FileNotFoundException] {
    Write-Output "File not found"
}
catch {
    Write-Output "Some other error: $_"
}

Real-World Specific Catches

try {
    $user = Get-ADUser -Identity "nonexistent" -ErrorAction Stop
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
    Write-Output "User not found in Active Directory"
    # Maybe create the user?
}
catch [System.UnauthorizedAccessException] {
    Write-Output "Insufficient permissions to query AD"
}
catch {
    Write-Output "Unexpected error: $($_.Exception.GetType().Name)"
    Write-Output $_.Exception.Message
}

Common Use Cases

Use Case 1: File Operations with Error Handling

What it does: Safely read a file with fallback options

function Get-ConfigFile {
    param(
        [string]$Path = "C:\config.txt",
        [string]$BackupPath = "C:\config.backup.txt"
    )

    try {
        $content = Get-Content -Path $Path -ErrorAction Stop
        Write-Output "Loaded config from primary location"
        return $content
    }
    catch [System.IO.FileNotFoundException] {
        Write-Warning "Primary config not found, trying backup..."

        try {
            $content = Get-Content -Path $BackupPath -ErrorAction Stop
            Write-Output "Loaded config from backup location"
            return $content
        }
        catch {
            Write-Error "No config file found. Using defaults."
            return @("default=value")
        }
    }
    catch {
        Write-Error "Unexpected error loading config: $_"
        throw  # Re-throw to caller
    }
}

Use Case 2: Network Operations with Retry

What it does: Retry failed network operations

function Test-ServerConnection {
    param(
        [string]$ComputerName,
        [int]$MaxRetries = 3
    )

    $attempt = 0
    $success = $false

    while ($attempt -lt $MaxRetries -and -not $success) {
        $attempt++

        try {
            Write-Output "Attempt $attempt of $MaxRetries..."

            $result = Test-Connection -ComputerName $ComputerName -Count 1 -ErrorAction Stop

            Write-Output "Connection successful!"
            $success = $true
            return $true
        }
        catch {
            Write-Warning "Attempt $attempt failed: $($_.Exception.Message)"

            if ($attempt -lt $MaxRetries) {
                Write-Output "Waiting 5 seconds before retry..."
                Start-Sleep -Seconds 5
            }
        }
    }

    if (-not $success) {
        Write-Error "Failed to connect after $MaxRetries attempts"
        return $false
    }
}

Use Case 3: Database Connection Cleanup

What it does: Ensure database connections are always closed

function Invoke-DatabaseQuery {
    param(
        [string]$ConnectionString,
        [string]$Query
    )

    $connection = $null
    $command = $null

    try {
        # Open connection
        $connection = New-Object System.Data.SqlClient.SqlConnection($ConnectionString)
        $connection.Open()
        Write-Output "Database connection opened"

        # Execute query
        $command = $connection.CreateCommand()
        $command.CommandText = $Query
        $result = $command.ExecuteScalar()

        Write-Output "Query executed successfully"
        return $result
    }
    catch [System.Data.SqlClient.SqlException] {
        Write-Error "Database error: $($_.Exception.Message)"
        throw
    }
    catch {
        Write-Error "Unexpected error: $_"
        throw
    }
    finally {
        # Always cleanup, even if error occurred
        if ($null -ne $command) {
            $command.Dispose()
        }

        if ($null -ne $connection) {
            if ($connection.State -eq 'Open') {
                $connection.Close()
                Write-Output "Database connection closed"
            }
            $connection.Dispose()
        }
    }
}

Error Actions

ErrorAction Parameter

# SilentlyContinue - Suppress error, continue
Get-Item "C:\NoFile.txt" -ErrorAction SilentlyContinue
Write-Output "Script continues"

# Stop - Make it a terminating error (catchable)
try {
    Get-Item "C:\NoFile.txt" -ErrorAction Stop
}
catch {
    Write-Output "Caught!"
}

# Continue - Show error, continue (default)
Get-Item "C:\NoFile.txt" -ErrorAction Continue
Write-Output "Keeps running"

# Inquire - Ask user what to do
Get-Item "C:\NoFile.txt" -ErrorAction Inquire

# Ignore - Completely ignore (not even added to $Error)
Get-Item "C:\NoFile.txt" -ErrorAction Ignore

ErrorActionPreference Variable

# Set for entire script/session
$ErrorActionPreference = "Stop"  # All errors are now terminating

try {
    Get-Item "C:\NoFile.txt"  # Now catchable without -ErrorAction
}
catch {
    Write-Output "Caught without -ErrorAction Stop"
}

# Reset to default
$ErrorActionPreference = "Continue"

The $Error Variable

# $Error is an array of all errors in the session
$Error.Clear()  # Clear error history

Get-Item "C:\DoesNotExist.txt" -ErrorAction SilentlyContinue
Get-Process "NonExistentProcess" -ErrorAction SilentlyContinue

# View all errors
$Error.Count  # 2

# Most recent error
$Error[0]

# View all errors with details
$Error | ForEach-Object {
    Write-Output "Error: $($_.Exception.Message)"
}

# Clear errors
$Error.Clear()

Real-World Examples

Example: Robust File Copy with Logging

Scenario: Copy files with comprehensive error handling and logging

function Copy-FileWithRetry {
    param(
        [string]$Source,
        [string]$Destination,
        [int]$MaxRetries = 3
    )

    # Validate source exists
    if (-not (Test-Path $Source)) {
        throw "Source file not found: $Source"
    }

    $attempt = 0
    $success = $false

    while ($attempt -lt $MaxRetries -and -not $success) {
        $attempt++

        try {
            Write-Output "[$attempt/$MaxRetries] Copying $Source to $Destination..."

            Copy-Item -Path $Source -Destination $Destination -ErrorAction Stop -Force

            # Verify copy
            if (Test-Path $Destination) {
                $sourceHash = (Get-FileHash -Path $Source).Hash
                $destHash = (Get-FileHash -Path $Destination).Hash

                if ($sourceHash -eq $destHash) {
                    Write-Output "Copy successful and verified"
                    $success = $true
                } else {
                    throw "File copied but hash mismatch"
                }
            }
        }
        catch [System.IO.IOException] {
            Write-Warning "IO Error on attempt $attempt : $($_.Exception.Message)"

            if ($attempt -lt $MaxRetries) {
                Start-Sleep -Seconds ([math]::Pow(2, $attempt))
            }
        }
        catch [System.UnauthorizedAccessException] {
            Write-Error "Access denied: $($_.Exception.Message)"
            break  # Don't retry permission errors
        }
        catch {
            Write-Warning "Unexpected error on attempt $attempt : $_"

            if ($attempt -lt $MaxRetries) {
                Start-Sleep -Seconds 3
            }
        }
    }

    if (-not $success) {
        throw "Failed to copy file after $MaxRetries attempts"
    }

    return $true
}

Example: Service Management with Error Handling

Scenario: Start a service with comprehensive error handling

function Start-ServiceSafely {
    param(
        [string]$ServiceName,
        [int]$TimeoutSeconds = 30
    )

    try {
        # Check if service exists
        $service = Get-Service -Name $ServiceName -ErrorAction Stop

        if ($service.Status -eq "Running") {
            Write-Output "$ServiceName is already running"
            return $true
        }

        Write-Output "Starting $ServiceName..."
        Start-Service -Name $ServiceName -ErrorAction Stop

        # Wait for service to start
        $service.WaitForStatus("Running", [TimeSpan]::FromSeconds($TimeoutSeconds))

        Write-Output "$ServiceName started successfully"
        return $true
    }
    catch [Microsoft.PowerShell.Commands.ServiceCommandException] {
        Write-Error "Service error: $($_.Exception.Message)"

        # Check if it's a dependency issue
        if ($_.Exception.Message -like "*dependencies*") {
            Write-Output "Checking service dependencies..."
            $service = Get-Service -Name $ServiceName
            $dependencies = $service.ServicesDependedOn

            foreach ($dep in $dependencies) {
                Write-Output "Dependency: $($dep.Name) - Status: $($dep.Status)"
            }
        }
        return $false
    }
    catch [System.TimeoutException] {
        Write-Error "Service did not start within $TimeoutSeconds seconds"
        return $false
    }
    catch {
        Write-Error "Unexpected error starting service: $_"
        return $false
    }
}

Tips & Tricks

Always Use -ErrorAction Stop for Catchable Errors

# Won't catch
try {
    Get-Item "C:\NoFile.txt"
} catch {
    # Doesn't run
}

# Will catch
try {
    Get-Item "C:\NoFile.txt" -ErrorAction Stop
} catch {
    # Runs!
}

Use Finally for Cleanup

# Good pattern for resource cleanup
try {
    $stream = [System.IO.File]::OpenRead("C:\file.txt")
    # Work with stream
}
catch {
    Write-Error "Error: $_"
}
finally {
    if ($stream) {
        $stream.Dispose()  # Always cleanup
    }
}

Re-throw Errors When Appropriate

function Get-Data {
    try {
        # Something that might fail
        Get-Item "C:\data.txt" -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to get data: $_"
        throw  # Re-throw to let caller handle it
    }
}

# Caller can now handle it
try {
    Get-Data
}
catch {
    Write-Output "Caller caught re-thrown error"
}

Don't Catch Everything Silently

# BAD: Hides all errors
try {
    # Complex code
}
catch {
    # Silent failure - no logging, no nothing
}

# GOOD: At least log it
try {
    # Complex code
}
catch {
    Write-Error "Error occurred: $_"
    # Maybe log to file
    Add-Content -Path "C:\Logs\errors.log" -Value "$(Get-Date): $_"
    throw  # Re-throw if caller needs to know
}

Watch Out for Non-Terminating Errors

# These won't be caught without -ErrorAction Stop
try {
    Get-Process "NoSuchProcess"  # Non-terminating
    Get-Item "C:\NoFile.txt"     # Non-terminating
    Write-Error "Custom error"   # Non-terminating
}
catch {
    # Doesn't run!
}

# Fix: Use -ErrorAction Stop
try {
    Get-Process "NoSuchProcess" -ErrorAction Stop
    Get-Item "C:\NoFile.txt" -ErrorAction Stop
    Write-Error "Custom error" -ErrorAction Stop
}
catch {
    # Now it runs
}

$ErrorActionPreference Scope

# Setting this affects EVERYTHING
$ErrorActionPreference = "Stop"

# Now ALL cmdlets behave differently
Get-Item "C:\NoFile.txt"  # Now throws, was non-terminating

# Be careful - might affect modules/functions you call
# Better to use -ErrorAction on individual cmdlets

Additional Resources