Error handling is a critical aspect of writing robust PowerShell scripts. Whether you’re automating system tasks, managing infrastructure, or developing complex solutions, proper error handling ensures your scripts fail gracefully and provide meaningful feedback. This comprehensive guide explores PowerShell’s error handling mechanisms including try-catch-finally blocks, throw statements, and best practices for production-ready scripts.
Understanding PowerShell Errors
PowerShell distinguishes between two fundamental types of errors: terminating and non-terminating errors. Understanding this distinction is crucial for effective error handling.
Terminating vs Non-Terminating Errors
Terminating errors halt script execution immediately and can be caught by try-catch blocks. Non-terminating errors generate error messages but allow the script to continue running. By default, most cmdlet errors are non-terminating.
# Non-terminating error example
Get-Item "C:\NonExistentFile.txt"
Write-Host "This line executes despite the error"
# Output:
# Get-Item : Cannot find path 'C:\NonExistentFile.txt' because it does not exist.
# This line executes despite the error
# Converting to terminating error
Get-Item "C:\NonExistentFile.txt" -ErrorAction Stop
Write-Host "This line will NOT execute"
# Output:
# Get-Item : Cannot find path 'C:\NonExistentFile.txt' because it does not exist.
# (Script stops here)
The Try-Catch-Finally Structure
The try-catch-finally construct provides structured error handling in PowerShell. The try block contains code that might generate errors, catch blocks handle specific exceptions, and the finally block executes cleanup code regardless of success or failure.
Basic Try-Catch Syntax
try {
# Code that might generate an error
$result = Get-Content "C:\ImportantFile.txt" -ErrorAction Stop
Write-Host "File content: $result"
}
catch {
# Error handling code
Write-Host "Error occurred: $_" -ForegroundColor Red
Write-Host "Error details: $($_.Exception.Message)"
}
# Output (if file doesn't exist):
# Error occurred: Get-Content : Cannot find path 'C:\ImportantFile.txt'...
# Error details: Cannot find path 'C:\ImportantFile.txt' because it does not exist.
The $_ Automatic Variable
Inside a catch block, the $_ variable represents the error object and provides access to detailed error information.
try {
1 / 0
}
catch {
Write-Host "Exception Type: $($_.Exception.GetType().FullName)"
Write-Host "Error Message: $($_.Exception.Message)"
Write-Host "Stack Trace: $($_.ScriptStackTrace)"
}
# Output:
# Exception Type: System.Management.Automation.RuntimeException
# Error Message: Attempted to divide by zero.
# Stack Trace: at <ScriptBlock>, <No file>: line 2
Multiple Catch Blocks
PowerShell allows multiple catch blocks to handle different exception types specifically. This enables targeted error handling based on the error that occurred.
try {
$number = "abc"
[int]$converted = $number
}
catch [System.InvalidCastException] {
Write-Host "Cannot convert '$number' to integer" -ForegroundColor Yellow
}
catch [System.IO.FileNotFoundException] {
Write-Host "File not found" -ForegroundColor Red
}
catch {
Write-Host "An unexpected error occurred: $_" -ForegroundColor Red
}
# Output:
# Cannot convert 'abc' to integer
Real-World Multi-Catch Example
function Connect-Database {
param([string]$ConnectionString)
try {
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $ConnectionString
$connection.Open()
Write-Host "Database connection successful" -ForegroundColor Green
return $connection
}
catch [System.Data.SqlClient.SqlException] {
Write-Host "SQL Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Error Number: $($_.Exception.Number)"
}
catch [System.InvalidOperationException] {
Write-Host "Invalid connection operation: $($_.Exception.Message)"
}
catch {
Write-Host "Unexpected database error: $_"
}
finally {
Write-Host "Connection attempt completed"
}
}
# Usage:
Connect-Database -ConnectionString "Server=localhost;Database=TestDB"
The Finally Block
The finally block executes regardless of whether an error occurs, making it ideal for cleanup operations like closing connections, releasing resources, or logging.
$file = $null
try {
$file = [System.IO.File]::OpenRead("C:\Data\log.txt")
$content = [System.IO.StreamReader]::new($file).ReadToEnd()
Write-Host "File read successfully"
}
catch {
Write-Host "Error reading file: $_" -ForegroundColor Red
}
finally {
if ($file) {
$file.Close()
Write-Host "File handle closed"
}
}
# Output (if successful):
# File read successfully
# File handle closed
# Output (if error):
# Error reading file: ...
# File handle closed
Finally Block Execution Guarantee
function Test-FinallyExecution {
try {
Write-Host "Try block executing"
throw "Simulated error"
Write-Host "This line won't execute"
}
catch {
Write-Host "Catch block: $($_.Exception.Message)"
return # Even with return, finally executes
}
finally {
Write-Host "Finally block ALWAYS executes"
}
Write-Host "This line won't execute due to return"
}
Test-FinallyExecution
# Output:
# Try block executing
# Catch block: Simulated error
# Finally block ALWAYS executes
The Throw Statement
The throw statement generates terminating errors, allowing you to create custom exceptions and control script flow based on validation logic.
Basic Throw Usage
function Divide-Numbers {
param(
[int]$Numerator,
[int]$Denominator
)
if ($Denominator -eq 0) {
throw "Division by zero is not allowed"
}
return $Numerator / $Denominator
}
try {
$result = Divide-Numbers -Numerator 10 -Denominator 0
}
catch {
Write-Host "Error: $_" -ForegroundColor Red
}
# Output:
# Error: Division by zero is not allowed
Throwing Custom Exception Objects
function Validate-Age {
param([int]$Age)
if ($Age -lt 0) {
$exception = [System.ArgumentException]::new("Age cannot be negative")
throw $exception
}
if ($Age -gt 150) {
throw [System.ArgumentOutOfRangeException]::new(
"Age",
$Age,
"Age seems unrealistic"
)
}
Write-Host "Valid age: $Age"
}
try {
Validate-Age -Age 200
}
catch [System.ArgumentOutOfRangeException] {
Write-Host "Out of range: $($_.Exception.Message)"
Write-Host "Actual value: $($_.Exception.ActualValue)"
}
catch [System.ArgumentException] {
Write-Host "Invalid argument: $($_.Exception.Message)"
}
# Output:
# Out of range: Age seems unrealistic (Parameter 'Age')
# Actual value: 200
ErrorAction Preference
The ErrorAction parameter controls how cmdlets respond to non-terminating errors. Understanding these options is essential for effective error handling.
| ErrorAction Value | Behavior | Use Case |
|---|---|---|
| Continue | Display error, continue execution (default) | General scripting |
| Stop | Treat as terminating error | When errors must halt execution |
| SilentlyContinue | Suppress error, continue | Expected errors you want to ignore |
| Ignore | Don’t display or log, continue | Performance-critical scenarios |
| Inquire | Prompt user for action | Interactive scripts |
# Example using different ErrorAction values
$files = @("C:\Exists.txt", "C:\DoesNotExist.txt", "C:\Another.txt")
foreach ($file in $files) {
# SilentlyContinue - suppress error messages
$content = Get-Content $file -ErrorAction SilentlyContinue
if ($content) {
Write-Host "Read $file successfully"
} else {
Write-Host "Skipped $file - not found" -ForegroundColor Yellow
}
}
Global Error Preference
# Set global error preference
$ErrorActionPreference = "Stop"
try {
Get-Item "C:\NonExistent.txt" # Now treated as terminating
Write-Host "This won't execute"
}
catch {
Write-Host "Caught error due to Stop preference"
}
# Reset to default
$ErrorActionPreference = "Continue"
Practical Error Handling Patterns
File Operations with Comprehensive Error Handling
function Copy-FileWithRetry {
param(
[string]$Source,
[string]$Destination,
[int]$MaxRetries = 3
)
$attempt = 0
$success = $false
while (-not $success -and $attempt -lt $MaxRetries) {
$attempt++
Write-Host "Attempt $attempt of $MaxRetries"
try {
if (-not (Test-Path $Source)) {
throw [System.IO.FileNotFoundException]::new(
"Source file not found: $Source"
)
}
Copy-Item $Source $Destination -ErrorAction Stop
$success = $true
Write-Host "File copied successfully" -ForegroundColor Green
}
catch [System.IO.FileNotFoundException] {
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
break # Don't retry if source doesn't exist
}
catch [System.UnauthorizedAccessException] {
Write-Host "Access denied. Check permissions." -ForegroundColor Red
Start-Sleep -Seconds 2
}
catch {
Write-Host "Unexpected error: $_" -ForegroundColor Red
if ($attempt -lt $MaxRetries) {
Write-Host "Retrying in 2 seconds..."
Start-Sleep -Seconds 2
}
}
}
return $success
}
# Usage:
$result = Copy-FileWithRetry -Source "C:\Source\data.txt" -Destination "C:\Dest\data.txt"
if ($result) {
Write-Host "Operation completed successfully"
} else {
Write-Host "Operation failed after all retries"
}
Web Request Error Handling
function Invoke-WebRequestSafe {
param([string]$Url)
try {
$response = Invoke-WebRequest -Uri $Url -ErrorAction Stop
Write-Host "Status: $($response.StatusCode)"
Write-Host "Content length: $($response.Content.Length) bytes"
return $response
}
catch [System.Net.WebException] {
$statusCode = $_.Exception.Response.StatusCode.value__
switch ($statusCode) {
404 { Write-Host "Error 404: Page not found" -ForegroundColor Yellow }
401 { Write-Host "Error 401: Unauthorized access" -ForegroundColor Red }
500 { Write-Host "Error 500: Server error" -ForegroundColor Red }
default { Write-Host "Web error ($statusCode): $($_.Exception.Message)" }
}
}
catch [System.Net.Sockets.SocketException] {
Write-Host "Network connection failed: $($_.Exception.Message)"
}
catch {
Write-Host "Unexpected error: $($_.Exception.GetType().Name)"
Write-Host "Message: $($_.Exception.Message)"
}
return $null
}
# Usage:
$response = Invoke-WebRequestSafe -Url "https://api.example.com/data"
Error Logging Best Practices
function Write-ErrorLog {
param(
[Parameter(Mandatory)]
[System.Management.Automation.ErrorRecord]$ErrorRecord,
[string]$LogPath = "C:\Logs\errors.log"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = @"
[$timestamp] ERROR
Message: $($ErrorRecord.Exception.Message)
Type: $($ErrorRecord.Exception.GetType().FullName)
Script: $($ErrorRecord.InvocationInfo.ScriptName)
Line: $($ErrorRecord.InvocationInfo.ScriptLineNumber)
Stack Trace: $($ErrorRecord.ScriptStackTrace)
---
"@
try {
$logEntry | Out-File -FilePath $LogPath -Append -ErrorAction Stop
}
catch {
Write-Warning "Failed to write to log file: $_"
}
}
# Usage in try-catch:
try {
Get-Item "C:\NonExistent.txt" -ErrorAction Stop
}
catch {
Write-ErrorLog -ErrorRecord $_
Write-Host "Error logged. Details: $($_.Exception.Message)"
}
Structured Error Handling Function
function Invoke-SafeOperation {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock]$Operation,
[string]$OperationName = "Operation",
[switch]$ThrowOnError
)
$result = @{
Success = $false
Data = $null
Error = $null
}
try {
Write-Verbose "Starting: $OperationName"
$result.Data = & $Operation
$result.Success = $true
Write-Verbose "Completed: $OperationName"
}
catch {
$result.Error = @{
Message = $_.Exception.Message
Type = $_.Exception.GetType().Name
StackTrace = $_.ScriptStackTrace
}
Write-Error "Failed: $OperationName - $($_.Exception.Message)"
if ($ThrowOnError) {
throw
}
}
finally {
Write-Verbose "Cleanup for: $OperationName"
}
return $result
}
# Usage example:
$result = Invoke-SafeOperation -OperationName "File Read" -Operation {
Get-Content "C:\Data\config.json" -ErrorAction Stop | ConvertFrom-Json
}
if ($result.Success) {
Write-Host "Data retrieved: $($result.Data.Count) items"
} else {
Write-Host "Operation failed: $($result.Error.Message)"
}
Advanced Error Handling Techniques
Custom Error Records
function New-CustomError {
param(
[string]$Message,
[string]$ErrorId,
[System.Management.Automation.ErrorCategory]$Category = 'NotSpecified'
)
$exception = [System.Exception]::new($Message)
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$exception,
$ErrorId,
$Category,
$null
)
return $errorRecord
}
function Process-CriticalData {
param([string]$Data)
if ([string]::IsNullOrWhiteSpace($Data)) {
$errorRecord = New-CustomError `
-Message "Data parameter cannot be empty" `
-ErrorId "EmptyDataError" `
-Category InvalidArgument
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
Write-Host "Processing: $Data"
}
try {
Process-CriticalData -Data ""
}
catch {
Write-Host "Custom error caught: $($_.FullyQualifiedErrorId)"
Write-Host "Category: $($_.CategoryInfo.Category)"
}
# Output:
# Custom error caught: EmptyDataError
# Category: InvalidArgument
Nested Try-Catch Blocks
function Process-DataPipeline {
param([string]$InputFile, [string]$OutputFile)
try {
# Outer try: Overall operation
Write-Host "Starting data pipeline"
$data = $null
try {
# Inner try: File reading
$data = Get-Content $InputFile -ErrorAction Stop
Write-Host "Data loaded: $($data.Count) lines"
}
catch {
Write-Host "Failed to read input file: $_"
throw "Input file error"
}
# Process data
$processed = $data | ForEach-Object { $_.ToUpper() }
try {
# Inner try: File writing
$processed | Out-File $OutputFile -ErrorAction Stop
Write-Host "Data written successfully"
}
catch {
Write-Host "Failed to write output file: $_"
throw "Output file error"
}
Write-Host "Pipeline completed successfully"
}
catch {
Write-Host "Pipeline failed: $_" -ForegroundColor Red
# Cleanup or rollback logic here
}
}
# Usage:
Process-DataPipeline -InputFile "C:\Data\input.txt" -OutputFile "C:\Data\output.txt"
Common Pitfalls and Solutions
Pitfall 1: Not Converting Non-Terminating Errors
# WRONG - try-catch won't catch non-terminating errors
try {
Get-Item "C:\NonExistent.txt" # Non-terminating by default
Write-Host "This line executes"
}
catch {
Write-Host "This won't catch the error"
}
# CORRECT - Use -ErrorAction Stop
try {
Get-Item "C:\NonExistent.txt" -ErrorAction Stop
Write-Host "This line won't execute"
}
catch {
Write-Host "Error caught successfully"
}
Pitfall 2: Swallowing Errors
# WRONG - Empty catch block hides problems
try {
# Some operation
}
catch {
# Silent failure - very bad practice
}
# CORRECT - Always log or handle errors
try {
# Some operation
}
catch {
Write-Warning "Operation failed: $($_.Exception.Message)"
# Log to file, send alert, etc.
}
Pitfall 3: Not Cleaning Up Resources
# WRONG - Resource leak if error occurs
$stream = [System.IO.File]::OpenRead("file.txt")
$content = [System.IO.StreamReader]::new($stream).ReadToEnd()
$stream.Close() # Won't execute if error occurs above
# CORRECT - Use finally for cleanup
$stream = $null
try {
$stream = [System.IO.File]::OpenRead("file.txt")
$content = [System.IO.StreamReader]::new($stream).ReadToEnd()
}
catch {
Write-Error "File operation failed: $_"
}
finally {
if ($stream) {
$stream.Close()
}
}
Error Handling Checklist
- Use
-ErrorAction Stopwith cmdlets in try blocks to catch non-terminating errors - Implement specific catch blocks for different exception types when appropriate
- Always include finally blocks for resource cleanup operations
- Log errors with sufficient detail for troubleshooting (timestamp, message, stack trace)
- Validate input parameters and throw meaningful custom errors
- Use
$ErrorActionPreferencecarefully and reset it when necessary - Never use empty catch blocks that silently swallow errors
- Consider retry logic for transient failures (network, file locks)
- Test error paths as thoroughly as success paths
- Document expected exceptions in function comments
Performance Considerations
# Measure try-catch overhead
Measure-Command {
1..10000 | ForEach-Object {
try {
$result = $_ * 2
}
catch {
# Handle error
}
}
}
# Output example:
# TotalMilliseconds : 245.8
# Without try-catch (for comparison)
Measure-Command {
1..10000 | ForEach-Object {
$result = $_ * 2
}
}
# Output example:
# TotalMilliseconds : 198.3
Try-catch blocks have minimal overhead, but avoid placing them in tight loops with millions of iterations. Instead, wrap the entire loop or use conditional logic to prevent errors.
Conclusion
Mastering error handling in PowerShell is essential for creating production-ready scripts and automation solutions. The try-catch-finally construct, combined with throw statements and proper ErrorAction usage, provides a robust framework for managing exceptions. Remember to catch specific exception types when possible, always clean up resources in finally blocks, and log errors comprehensively for troubleshooting. By following the patterns and best practices outlined in this guide, you’ll write more reliable, maintainable PowerShell code that handles both expected and unexpected failures gracefully.







