Error Handling & Basic Debugging

PowerShell Fundamentals

Chapter 9 · Error Handling & Basic Debugging

try/catch looks exactly like it does in Java, JavaScript, or C# — which is precisely what makes PowerShell's own version so easy to get wrong the first time. Most built-in cmdlets don't actually stop and throw the way an exception would in those languages; they print a red error and simply move on to the next thing. A catch block wrapped around one of those cmdlets can sit there, correctly written, and never run at all. This chapter is about why, and about the one setting that fixes it.

Two Kinds of Errors: Terminating vs. Non-Terminating

PowerShell has two genuinely different error categories, and the distinction isn't cosmetic:

try { Get-Item C:\does-not-exist.txt "This line still runs!" } catch { "This never prints — the catch block never fires" } # Get-Item's own error is NON-TERMINATING by default — PowerShell prints it in red and moves on, # which is why the very next line inside the try block still executed normally

Most cmdlet errors — a missing file, an unreachable path, a permission failure — are non-terminating by default: PowerShell displays the error and simply continues to the next statement, exactly as if nothing had interrupted it. try/catch only intercepts terminating errors — ones that actually stop execution at that point. A non-terminating error inside a try block just runs its course and lets the rest of the block keep going, right past the catch entirely.

-ErrorAction Stop: Making try/catch Actually Work

Nearly every cmdlet accepts an -ErrorAction parameter, and Stop is the one that converts a non-terminating error into a genuine terminating one catch can intercept:

try { Get-Item C:\does-not-exist.txt -ErrorAction Stop "This line never runs — the error above stopped execution" } catch { "Caught it: $($_.Exception.Message)" } # Caught it: Cannot find path 'C:\does-not-exist.txt' because it does not exist.
-ErrorAction valueBehavior
ContinueDefault — display the error, keep running (non-terminating; catch can't see it)
StopTurns the error terminating — the only value that reliably makes catch fire
SilentlyContinueSuppress the display, keep running — the error still lands in $Error below
IgnoreSuppress the display and skip $Error too — the error leaves no trace at all
InquirePrompt interactively for what to do
The central fact this chapter is built on
A try/catch block around a cmdlet call is not, by itself, real error handling — it's only real error handling once you've confirmed that cmdlet's failure is actually terminating, almost always by adding -ErrorAction Stop yourself. Skipping this is the single most common reason a PowerShell script's error handling silently does nothing: the code looks defensive, reads cleanly, and simply never activates.

$_ Inside catch: A Completely Different Meaning

The same symbol, two unrelated meanings
Chapter 4 established $_ as "the current object flowing through the pipeline," inside Where-Object or ForEach-Object. Inside a catch block, $_ means something entirely different: the error record that was just caught — not your data at all. $_.Exception.Message is how you pull the human-readable error text out of it. There's no relationship between these two uses beyond sharing the same variable name; which one applies is decided purely by which kind of block you're currently inside.

$Error: The Session's Own Error History

$Error[0] # the most recent error, even ones you didn't try/catch $Error.Count # how many errors have accumulated this session $Error.Clear() # empty it out, e.g. before a section of a script you want to check cleanly

$Error collects every error PowerShell has recorded this session — including SilentlyContinue ones from the table above — regardless of whether anything actually caught them. It's the automatic variable Chapter 5 flagged but deferred; this is its real, practical use.

finally: Always Runs

try { Get-Content .\data.txt -ErrorAction Stop } catch { "Something went wrong: $($_.Exception.Message)" } finally { "Done — this runs whether it succeeded or failed" }

finally runs regardless of outcome — success, a caught error, or even an uncaught one — which makes it the right place for cleanup that must always happen (closing a connection, releasing a lock) rather than cleanup that only matters on failure.

Write-Verbose: The Safe Status Channel Promised in Chapter 7

Chapter 7 flagged Write-Verbose as a safer alternative to a bare, uncaptured status message — here's why: it's completely hidden by default, and only appears when the caller explicitly opts in with -Verbose:

function Get-Square { param([int]$n) Write-Verbose "Squaring $n" $n * $n } Get-Square 4 # just 16 — the Write-Verbose line stays silent Get-Square 4 -Verbose # shows "VERBOSE: Squaring 4" AND still returns 16

Unlike Write-Output (Chapter 7), a Write-Verbose call never joins the function's return value under any circumstances — it writes to a genuinely separate stream that only -Verbose reveals.

A first practical habit
Whenever you write try/catch around a built-in cmdlet, add -ErrorAction Stop to that cmdlet call as a reflex — before assuming your error handling works, not after debugging why it silently didn't.

Hands-On Exercises

Exercise 1

Explain why wrapping Get-Item C:\does-not-exist.txt in try/catch (with no -ErrorAction) never triggers the catch block. What single change fixes it?

📄 View solution
Exercise 2

Explain why $_ means something completely different inside a catch block versus inside a Where-Object script block, referencing Chapter 4's own original meaning for $_.

📄 View solution
Exercise 3

Write a try/catch/finally block that attempts Get-Content -ErrorAction Stop on a file, prints a friendly message using $_.Exception.Message in catch, and always prints "Done" in finally regardless of success or failure.

📄 View solution

Chapter 9 Quick Reference

  • Non-terminating vs. terminating errors — most cmdlet errors are non-terminating by default; only terminating errors reach catch
  • -ErrorAction Stop — converts a non-terminating error into one catch can actually intercept
  • -ErrorAction SilentlyContinue / Ignore / Inquire — suppress display (still logs to $Error) / suppress entirely / prompt
  • $_.Exception.Message — the readable error text, inside a catch block only
  • $_ inside catch — the caught error record, a completely different meaning from $_ inside Where-Object/ForEach-Object (Chapter 4)
  • $Error[0] / $Error.Count / $Error.Clear() — the session's running error history
  • finally — always runs, success or failure — the right place for guaranteed cleanup
  • Write-Verbose — hidden by default, shown with -Verbose, never joins the return value
  • Next chapter: Execution Policy & Script Security Basics