Skip to content
6 min read·Lesson 4 of 8

Error Handling, Debugging, and Logging

Terminating vs non-terminating errors, try/catch, ErrorAction, debugging with breakpoints, and structured logging patterns.

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

TerminatingNon-terminating
Stops the pipeline / function / scriptRecords the error, continues
Syntax errors, runtime exceptions, parameter binding failuresMost cmdlet failures (file not found, host unreachable)
Caught by try/catchNot 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
  • $_.Exception is the underlying .NET exception
  • $_.Exception.Message is the human message
  • $_.ScriptStackTrace is the call stack
  • $_.InvocationInfo gives 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:

StreamCmdletPurpose
Output (pipeline)Write-OutputData that flows downstream
ErrorWrite-ErrorErrors
WarningWrite-WarningConcerns short of error
VerboseWrite-VerboseDiagnostic chatter when -Verbose
DebugWrite-DebugDeveloper detail when -Debug
InformationWrite-InformationStructured progress info (PS5+)
Host (avoid)Write-HostDirect 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

  1. #Requires -Version 7.0 and #Requires -Modules @{ ModuleName="Az.Compute"; ModuleVersion="6.0" } at the top
  2. Set-StrictMode -Version Latest and $ErrorActionPreference = "Stop"
  3. [CmdletBinding(SupportsShouldProcess)] for destructive cmdlets, with ShouldProcess calls
  4. Parameter validation (ValidateSet, ValidateScript, type constraints)
  5. try/catch around external operations; meaningful error messages
  6. Structured logging — JSON to a log file or a real log service
  7. Pester tests for non-trivial functions
  8. 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.

Key Takeaways

  • PowerShell has terminating and non-terminating errors — by default many cmdlet failures DO NOT stop execution.
  • -ErrorAction Stop converts non-terminating errors so try/catch can catch them.
  • try/catch/finally is the standard error structure; $_ inside catch is the error record.
  • Set-PSDebug, Set-PSBreakpoint, and the VS Code PowerShell extension cover debugging.
  • Use Write-Verbose / Write-Information / Write-Error / Write-Warning — never Write-Host for diagnostics.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →