PowerShell Error Handling: Terminating vs Non-Terminating Errors

The Error Handling Trap That Catches Everyone
PowerShell error handling works reliably — until suddenly it does not. A try-catch block wraps a cmdlet call, an error occurs, but the catch block never fires. The script keeps running as if nothing happened. This baffling behavior has a specific cause: PowerShell has two fundamentally different error types, and try-catch only catches one of them. Understanding the distinction between terminating and non-terminating errors is the single most important concept in PowerShell error handling, and confusing them causes silent failures in production scripts.
Quick Answer
Terminating errors stop execution and are caught by try-catch. Non-terminating errors write to the error stream and continue execution — try-catch ignores them. Convert non-terminating errors to terminating ones with -ErrorAction Stop on any cmdlet, or set $ErrorActionPreference = 'Stop' at the script scope.
What Makes an Error Terminating vs Non-Terminating
The cmdlet author decides which error type to use. Commands that process multiple input objects typically emit non-terminating errors so that one failed object does not abort processing of the rest. For example, Get-Item on a list of paths emits a non-terminating error for each missing path and continues to return results for the valid paths. Commands that have a single point of failure, or cmdlets that explicitly call $PSCmdlet.ThrowTerminatingError(), produce terminating errors that stop the pipeline immediately.
# Non-terminating: error written to error stream, execution continues
Get-Item "C:\RealFolder", "C:\FakeFolder", "C:\AnotherRealFolder"
# Only FakeFolder errors — the other two items still return
Get-Item: Cannot find path 'C:\FakeFolder' because it does not exist.
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------
d---- 01/01/2024 12:00 RealFolder
d---- 01/01/2024 12:00 AnotherRealFolder
# Terminating: stops everything
try {
[xml]"<invalid xml"
}
catch {
Write-Host "Caught terminating error: $_"
}
How -ErrorAction Stop Converts Non-Terminating Errors
Adding -ErrorAction Stop to any cmdlet instructs it to throw a terminating error instead of writing a non-terminating one. This is the most targeted fix — you opt specific calls into terminating behavior without changing global settings. It is the recommended approach for most error handling scenarios in production scripts. The error object available in the catch block is identical whether the error was originally terminating or converted by -ErrorAction Stop.
# Without -ErrorAction Stop: catch does NOT fire
try {
Get-Item "C:\DoesNotExist"
}
catch {
Write-Host "This will never print"
}
# With -ErrorAction Stop: catch fires correctly
try {
Get-Item "C:\DoesNotExist" -ErrorAction Stop
}
catch {
Write-Host "Caught: $($_.Exception.Message)"
}
Get-Item: Cannot find path 'C:\DoesNotExist' because it does not exist.
Caught: Cannot find path 'C:\DoesNotExist' because it does not exist.
Write-Error vs throw vs $PSCmdlet.ThrowTerminatingError()
When writing your own functions, you have three ways to signal errors. Write-Error emits a non-terminating error — callers can suppress it or catch it only with -ErrorAction Stop. The throw statement emits a terminating error immediately, stopping execution and triggering catch blocks in callers. $PSCmdlet.ThrowTerminatingError() is the advanced module pattern that creates a proper ErrorRecord with full metadata, but requires the function to use [CmdletBinding()].
function Test-ErrorTypes {
[CmdletBinding()]
param([string]$Mode)
switch ($Mode) {
"nonterminating" {
Write-Error "Non-terminating — execution continues after this"
}
"terminating" {
throw "Terminating — stops execution immediately"
}
"cmdlet" {
$err = [System.Management.Automation.ErrorRecord]::new(
[Exception]::new("Proper cmdlet terminating error"),
"CustomErrorId",
[System.Management.Automation.ErrorCategory]::InvalidOperation,
$Mode
)
$PSCmdlet.ThrowTerminatingError($err)
}
}
}
try { Test-ErrorTypes -Mode "terminating" }
catch { Write-Host "Caught: $_" }
The $Error Automatic Variable and ErrorVariable
PowerShell maintains a session-wide error collection in $Error. The most recent error is at index 0. This variable captures both terminating and non-terminating errors regardless of whether a try-catch is present. The -ErrorVariable parameter stores errors from a specific cmdlet invocation in a named variable, which is useful for inspecting errors from cmdlets that continue after emitting them. Clear $Error at the start of critical script sections to avoid inspecting stale errors.
$Error.Clear()
# Capture errors from a specific cmdlet without stopping execution
Get-Item "C:\Fake1","C:\Fake2" -ErrorVariable itemErrors -ErrorAction SilentlyContinue
Write-Host "Errors captured: $($itemErrors.Count)"
foreach ($e in $itemErrors) {
Write-Host " - $($e.Exception.Message)"
}
# Check $Error for all recent errors
Write-Host "Total session errors: $($Error.Count)"
if ($Error.Count -gt 0) {
Write-Host "Most recent: $($Error[0].Exception.Message)"
}
Try-Catch Only Catches Terminating Errors
This is the root cause of most PowerShell error handling confusion. try-catch is a terminating error handler. Non-terminating errors bypass it entirely and land in $Error and the error stream. The correct fix is always -ErrorAction Stop on the cmdlet, or setting $ErrorActionPreference = 'Stop' for a broader scope. The latter approach is powerful but breaks scripts that intentionally rely on non-terminating behavior to continue processing after partial failures.
# WRONG: try-catch silently ignored for non-terminating errors
try {
Get-Service "NonExistentService"
Write-Host "Reached here despite error" # This WILL print
}
catch {
Write-Host "This will NOT print"
}
# CORRECT: -ErrorAction Stop makes the catch fire
try {
Get-Service "NonExistentService" -ErrorAction Stop
Write-Host "Will not reach here"
}
catch {
Write-Host "Correctly caught: $($_.Exception.Message)"
}
Patterns for Reliable Error Capture in Pipelines
Pipeline error handling requires care because -ErrorAction Stop on a pipeline source will abort the entire pipeline on the first error. For pipelines that should process all items and collect errors for later review, use -ErrorVariable with -ErrorAction SilentlyContinue to accumulate failures without stopping. Review the error collection after the pipeline completes and handle each failure appropriately.
$paths = @("C:\Windows","C:\Fake","C:\Program Files","C:\AlsoFake")
$results = $paths | ForEach-Object {
Get-Item $_ -ErrorAction SilentlyContinue -ErrorVariable +pipeErrors
}
Write-Host "Successful: $($results.Count)"
Write-Host "Failed : $($pipeErrors.Count)"
$pipeErrors | ForEach-Object {
Write-Warning "Failed: $($_.Exception.Message)"
}
Common Errors
- Try-catch block does not fire for non-terminating errors. The cmdlet continues past the catch block silently. The fix is always adding
-ErrorAction Stopto the specific cmdlet call inside the try block. Do not restructure your catch logic — restructure the error type. - Setting $ErrorActionPreference = ‘Stop’ globally breaks scripts. Some scripts and modules deliberately rely on non-terminating errors to continue processing partial failures. Changing this preference at the script scope affects all cmdlets including those inside called modules. Scope it narrowly with
-ErrorAction Stopon individual calls, or restore the preference in a finally block.
Related Cmdlets / See Also
Wrapping Up
Every PowerShell developer hits the silent try-catch bug eventually. The fix is simple once the distinction is understood: add -ErrorAction Stop to make any cmdlet’s errors catchable. Use -ErrorVariable when you need to collect non-terminating errors without stopping a pipeline. Apply $ErrorActionPreference carefully and narrowly to avoid unexpected side effects across your entire script.


