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."
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?
- Silent failures: by default, many PowerShell errors are non-terminating—the script continues and you never know it skipped 20 servers.
- Cleanup: the finally block runs whether or not an error occurred, ensuring connections are closed and locks are released.
- Pipeline exit codes: CI/CD needs a non-zero exit code on failure; unhandled errors may still exit 0, marking a broken build as "passed."
- Context in failures: catching errors lets you add context—server name, function name, input value—to the error message before logging 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.
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
⌨️ Commands / Syntax
# 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
- Set
$ErrorActionPreference = 'Stop'and try runningGet-Item C:\nonexistent. It should now throw instead of printing a red error and continuing. - Wrap the same command in try/catch and handle the FileNotFoundException specifically.
- Read
$Error[0].Exception.Messageand$Error[0].InvocationInfo.PositionMessageafter a caught error to inspect its contents. - Add
Write-Verboselines to a function and test with-Verboseon and off. - Call an external command (like
git statusin a non-repo folder) and check$LASTEXITCODE.
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.
- Cause: the script uses
ErrorAction SilentlyContinuein several places, suppressing errors and never setting$LASTEXITCODEor throwing. - Diagnose: remove SilentlyContinue and add
-Verboseto see which operations are being silently swallowed. Check if$Errorcontains errors after a run that appeared successful. - Fix: change to
$ErrorActionPreference = 'Stop'globally, use try/catch to handle expected failures explicitly, and addexit 1(orthrow) at the end when any failure was accumulated. The pipeline will then correctly fail the build.
🎯 Interview Questions
Beginner
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.
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.
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
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.
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.