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:
- Terminating Errors: These stop script execution completely
- 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