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
throwstatements - .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:
- PowerShell executes code in the
tryblock - If a terminating error occurs, execution immediately jumps to
catch - The
catchblock handles the error (logging, cleanup, fallback logic, etc.) - Execution continues after the
catchblock
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 descriptionException.GetType(): Specific exception type for type-specific handlingTargetObject: The object that caused the errorCategoryInfo: 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 executionbreak: 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
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
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 continuethrow: 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.
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 →