One of the surprising bits of PowerShell coming from other languages: by default, many cmdlet errors do not stop the script. This is intentional — when you process 1000 files and 3 fail, you usually want the other 997 to keep going. But it surprises people, and getting the right behaviour is critical for production scripts.
Terminating vs Non-Terminating
| Terminating | Non-terminating |
|---|---|
| Stops the pipeline / function / script | Records the error, continues |
| Syntax errors, runtime exceptions, parameter binding failures | Most cmdlet failures (file not found, host unreachable) |
| Caught by try/catch | Not caught by try/catch (unless promoted) |
You promote a non-terminating error to terminating with -ErrorAction Stop:
Get-Item missing.txt # writes error, continues
Get-Item missing.txt -ErrorAction Stop # throws; can be caught
try / catch / finally
try {
$config = Get-Content config.json -ErrorAction Stop | ConvertFrom-Json
Connect-AzAccount -ErrorAction Stop
# ... work ...
}
catch [System.IO.FileNotFoundException] {
Write-Error "Config file missing: $($_.Exception.Message)"
exit 1
}
catch {
Write-Error "Unexpected error: $($_.Exception.Message)"
Write-Error $_.ScriptStackTrace
throw # re-throw to propagate
}
finally {
if ($connection) { Disconnect-AzAccount }
}
Inside catch:
$_is the error record$_.Exceptionis the underlying .NET exception$_.Exception.Messageis the human message$_.ScriptStackTraceis the call stack$_.InvocationInfogives line, column, command
Multiple catch blocks by exception type work as expected.
The Error Variables
$Error— an array of recent errors;$Error[0]is the most recent$?— boolean: did the last command succeed$LASTEXITCODE— exit code of the last external command (not PowerShell cmdlets)
Clear with $Error.Clear(). Useful for "let me check whether anything went wrong in that block" patterns.
ErrorAction Preference
Each cmdlet accepts -ErrorAction. The default is set by $ErrorActionPreference:
- Continue (default) — display error, continue
- Stop — terminating; can be caught
- SilentlyContinue — suppress display, continue
- Ignore — also discard from
$Error - Inquire — prompt
For production scripts, set $ErrorActionPreference = "Stop" at the top so every error becomes terminating and is caught explicitly. This is the standard pattern.
throw and Write-Error
# Throw — raises a terminating error
if (-not $Path) { throw "Path is required" }
if (-not (Test-Path $Path)) { throw [System.IO.FileNotFoundException]::new("Missing: $Path") }
# Write-Error — non-terminating by default
Write-Error "Couldn't reach $url"
Write-Error -Exception ([Exception]::new("boom")) -Category InvalidOperation -ErrorId "BoomError"
Validating Outside the Function Body
Lesson 3 covered parameter validation. Prefer that over imperative checks in the body — validation runs before the function executes, with cleaner errors.
Logging — Don't Use Write-Host
PowerShell has streams beyond stdout. Use them appropriately:
| Stream | Cmdlet | Purpose |
|---|---|---|
| Output (pipeline) | Write-Output | Data that flows downstream |
| Error | Write-Error | Errors |
| Warning | Write-Warning | Concerns short of error |
| Verbose | Write-Verbose | Diagnostic chatter when -Verbose |
| Debug | Write-Debug | Developer detail when -Debug |
| Information | Write-Information | Structured progress info (PS5+) |
| Host (avoid) | Write-Host | Direct to console — bypasses pipeline, not redirectable |
For structured logging in production scripts, ship to a file or a system log:
function Write-Log {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)] [string] $Message,
[ValidateSet("INFO","WARN","ERROR")] [string] $Level = "INFO"
)
process {
$entry = [pscustomobject]@{
Timestamp = (Get-Date).ToString("o")
Level = $Level
Message = $Message
Host = $env:COMPUTERNAME
}
$entry | ConvertTo-Json -Compress | Add-Content -Path $env:LOG_FILE
}
}
Or use a community module — Logging on PSGallery covers log levels, multiple targets (file, console, Slack, Loggly), and rotation.
Debugging
Breakpoints
Set-PSBreakpoint -Script ./deploy.ps1 -Line 42
Set-PSBreakpoint -Command Connect-AzAccount
Set-PSBreakpoint -Variable count -Mode Write
Hit a breakpoint and you enter a nested prompt where you can inspect variables, step (s), continue (c), or quit (q).
VS Code
The PowerShell extension for VS Code is the modern default for editing and debugging — F5 to debug, F9 for breakpoints, full IntelliSense, integrated terminal hosts the script.
Quick interactive debug tactic
# Drop a breakpoint mid-script:
Wait-Debugger # pauses at this line for an attached debugger
# Or interactive inspection:
$DebugPreference = "Continue" # shows Write-Debug output
Set-PSDebug -Trace 1 # prints each line as it executes (one level)
Strict Mode
Catch silly mistakes early:
Set-StrictMode -Version Latest
Now: using an uninitialised variable errors, referencing a non-existent property errors, calling functions with positional args after the param list errors. Worth running at the top of every production script.
Pester — Testing
Pester is the de facto unit-testing framework. Looks like Jest / RSpec:
Describe "Get-Greeting" {
It "uses 'world' by default" {
Get-Greeting | Should -Be "Hello, world!"
}
It "accepts a name" {
Get-Greeting -Name "Alice" | Should -Be "Hello, Alice!"
}
}
Run with Invoke-Pester. Mocks (Mock Get-AzVM { ... }), tags, and coverage all supported. CI-friendly via NUnit XML output.
Production Script Checklist
#Requires -Version 7.0and#Requires -Modules @{ ModuleName="Az.Compute"; ModuleVersion="6.0" }at the topSet-StrictMode -Version Latestand$ErrorActionPreference = "Stop"[CmdletBinding(SupportsShouldProcess)]for destructive cmdlets, withShouldProcesscalls- Parameter validation (
ValidateSet,ValidateScript, type constraints) - try/catch around external operations; meaningful error messages
- Structured logging — JSON to a log file or a real log service
- Pester tests for non-trivial functions
- Comment-based help on every public function
With this foundation you have everything you need to write production-grade PowerShell. The next four lessons apply it to the dominant use case: cloud and Microsoft 365 automation.