IntermediateLesson 7 of 16

Error Handling and Debugging

Control how PowerShell handles failures: understand terminating vs non-terminating errors, use try/catch/finally for structured error handling, inspect the $Error variable, and debug scripts with Set-PSDebug and Write-Verbose.

🧒 Simple Explanation (ELI5)

Errors in PowerShell are like speed bumps on a road. Some are small bumps the car can roll over (non-terminating errors—the script keeps going). Others are big crashes that stop the car entirely (terminating errors). A try/catch block is like a safety net under a trapeze artist: if the artist falls (an error happens), the net catches them and your cleanup code in finally runs regardless. Without error handling, one bad server in a loop of 50 stops the whole script.

🔧 Why Do We Need It?

⚙️ Technical Explanation

PowerShell has two error types: terminating errors stop execution immediately (thrown exceptions, parameter validation failures, methods that throw .NET exceptions). Non-terminating errors write to the error stream and continue execution by default.

$ErrorActionPreference (global) and -ErrorAction (per-cmdlet) control behavior: Continue (default, show error and continue), Stop (convert non-terminating to terminating), SilentlyContinue (suppress and continue), Ignore (suppress completely).

The $Error automatic variable is an array of all errors in the session. $Error[0] is the most recent. Each error has .Exception.Message, .InvocationInfo.Line, .ScriptStackTrace, and more.

🛑
Use -ErrorAction Stop in Scripts, Not SilentlyContinue

Scripts that run in CI/CD pipelines should default to $ErrorActionPreference = 'Stop' at the top. This makes all non-terminating errors behave like terminating ones, so your try/catch blocks can handle them. Only use SilentlyContinue where you explicitly expect and have planned for failure (checking if an optional resource exists). Silent failures in production scripts are the number one cause of "the pipeline passed but nothing deployed."

🔎
Add Context to Caught Errors

When you catch an error, add context before rethrowing or logging: catch { throw "Failed processing server '$server': $($_.Exception.Message)" }. The original error message alone ("Access denied") tells you nothing. Adding the server name, file path, or operation being attempted gives the log enough information to fix the issue without reproducing it.

📊 Visual Representation

try/catch/finally Flow
try { ... }
→ error?
catch { log + handle }
no error
→ skip catch →
finally { always runs }

⌨️ Commands / Syntax

powershell
# Make all errors terminating in production scripts
$ErrorActionPreference = 'Stop'

# Basic try/catch/finally
try {
    $result = Invoke-RestMethod -Uri "https://api.example.com/health"
    Write-Host "API status: $($result.status)"
}
catch [System.Net.WebException] {
    Write-Error "Network error reaching API: $($_.Exception.Message)"
}
catch {
    Write-Error "Unexpected error: $($_.Exception.Message)"
    Write-Error "Stack trace: $($_.ScriptStackTrace)"
}
finally {
    Write-Verbose "Health check attempt completed"
}

# Catch specific .NET exception types
try {
    [System.IO.File]::ReadAllText("C:\missing.txt")
}
catch [System.IO.FileNotFoundException] {
    Write-Warning "Config file not found. Using defaults."
}

# throw to create custom errors
function Validate-Environment {
    param([string]$Env)
    if ($Env -notin @('dev','staging','prod')) {
        throw [System.ArgumentException]"Invalid environment: $Env"
    }
}

# Check $Error for recent failures
try { Get-Item C:\nonexistent -ErrorAction Stop }
catch {}
$Error[0].Exception.Message
$Error[0].InvocationInfo.PositionMessage

# Debugging with Write-Verbose (only shows with -Verbose)
$VerbosePreference = 'Continue'  # or pass -Verbose to function
Write-Verbose "Processing server: $serverName"

# Set-PSDebug for step-through debugging (dev only)
Set-PSDebug -Trace 1   # trace each line
# ... run code ...
Set-PSDebug -Off        # disable tracing

# Check exit code from external commands
& git status
if ($LASTEXITCODE -ne 0) {
    throw "git status failed with exit code $LASTEXITCODE"
}

💼 Example (Real-world Use Case)

A deployment script iterates over 20 Azure web apps, calling the management API to trigger slot swaps. Without error handling, one API timeout stops the entire script and the remaining 19 apps are not deployed. With try/catch around each iteration, failures are logged with the app name, added to a $failures array, and the loop continues. At the end, if $failures.Count -gt 0, the script exits with a non-zero code and the CI/CD pipeline fails, triggering an alert with a complete list of failed apps.

🧪 Hands-on

  1. Set $ErrorActionPreference = 'Stop' and try running Get-Item C:\nonexistent. It should now throw instead of printing a red error and continuing.
  2. Wrap the same command in try/catch and handle the FileNotFoundException specifically.
  3. Read $Error[0].Exception.Message and $Error[0].InvocationInfo.PositionMessage after a caught error to inspect its contents.
  4. Add Write-Verbose lines to a function and test with -Verbose on and off.
  5. Call an external command (like git status in a non-repo folder) and check $LASTEXITCODE.
🎮
Try It Yourself

Write a function that accepts a list of server names and a service name. For each server, attempt Get-Service with -ErrorAction Stop inside a try/catch. Accumulate successful results in a $healthy array and failures (with server name and error message) in a $failed array. At the end, output a summary: "Checked X servers: Y healthy, Z failed" and list the failed servers. Exit with 1 if any failures occurred.

🐛 Debugging Scenario

Problem: a script runs successfully but the CI/CD pipeline shows it as passed even though several operations actually failed.

🎯 Interview Questions

Beginner

What is the difference between a terminating and non-terminating error?

A terminating error stops script execution immediately. A non-terminating error writes to the error stream and the script continues. Most cmdlet errors are non-terminating by default. Use -ErrorAction Stop to convert them to terminating so try/catch can handle them.

What does $ErrorActionPreference = 'Stop' do?

It converts all non-terminating errors into terminating errors globally for the script. This means every cmdlet failure will throw an exception that try/catch can handle, instead of silently continuing. It is the recommended setting for production and CI/CD automation scripts.

What does the finally block in a try/catch do?

The finally block always runs regardless of whether an error occurred. It is used for cleanup operations: closing connections, releasing file locks, deleting temp files, writing completion logs. Even if the catch block re-throws an error, the finally block still executes before the exception propagates.

Scenario-based

A CI/CD pipeline reports success but your deployment partially failed. How do you diagnose and prevent this?

The pipeline likely ran a script that swallowed errors with SilentlyContinue or did not set a non-zero exit code on failure. Add $ErrorActionPreference = 'Stop', wrap operations in try/catch to count failures, and add exit 1 or throw if any failure occurred. Check $LASTEXITCODE after external tool invocations (az, docker, git) and fail explicitly when they return non-zero.

You need to process 100 items in a loop and continue even when individual items fail, but report all failures at the end. How do you implement this?

Use a try/catch inside the foreach loop. On catch, add the item and error message to a $failures array—do not rethrow. After the loop, check if $failures.Count -gt 0 and throw or exit 1 with a summary of all failures. This gives you fail-fast visibility on the complete failure set without stopping after the first error.

🌐 Real-world Usage

Azure automation runbooks use try/catch to handle API timeouts and rate limits, retrying with exponential backoff. CI/CD deployment scripts use $ErrorActionPreference = 'Stop' and structured error accumulation to ensure pipelines fail when deployments fail, not just when unhandled exceptions occur. Operations monitoring scripts catch and log per-server failures without stopping the whole health-check run.

📝 Summary

Non-terminating errors are the silent killers of automation scripts. Use $ErrorActionPreference = 'Stop' to convert them to terminating errors and let try/catch handle them. Catch specific exception types for precise handling. Always add context to error messages before logging. Use finally for cleanup. Check $LASTEXITCODE after external command calls. Write-Verbose is your primary debugging tool in production-safe scripts.