Repository navigation
Our Error Handling, Ourselves - time to fully understand and properly document PowerShell's error handling #1583
Description
The existing help topics that touch on error handling (about_Throw,
about_CommonParameters,
about_Preference_Variables, about_Trap, about_Try_Catch_Finally
):
-
have sown longstanding confusion due to conflating the two distinct types of terminating errors:
- statement-terminating errors, as reported by cmdlets in certain non-recoverable situations (via the
.ThrowTerminatingError()method) and by expressions in which a .NET exception / a PS runtime error occurs. - script-terminating errors (fatal errors), as either triggered by
Throwor by escalating one of the other error types via error-action preference-variable / parameter valueStop.
- statement-terminating errors, as reported by cmdlets in certain non-recoverable situations (via the
-
have always contained the incorrect claim that the error-action preference / parameter values only pertain to non-terminating errors - which is true for the
-ErrorActionparameter, but not the$ErrorActionPreferencepreference variable - see Clarify the intended behavior/documentation/terminology of terminating errors PowerShell/PowerShell#4292-
However,
$ErrorActionPreferencewithSilentlyContinueandIgnore- which in isolation silences all errors, including terminating ones - is preempted by an enclosingtry/catchstatement or applicabletrapstatement. That is, in the presence of these constructs - only -$ErrorActionPreferencein effect then does not apply to terminating errors, which are then still caught - see Throw outside Try\Trap acts like a non-terminating error PowerShell/PowerShell#19500 (comment) -
Arguably,
-ErrorActiontoo should apply to all error types (as appropriate), though changing hat would certainly be a breaking change - see Cmdlet.ThrowTerminatingError() is not affected by-ErrorActionPowerShell/PowerShell#14819
-
It's time to:
-
correct the existing topics
-
provide a conceptual
about_Error_Handlingtopic, as Ashley McGlone (@GoateePFE) suggests - see Need an about_Error_Handling topic #1424
Below is my understanding of how PowerShell's error handling actually works as of Windows PowerShell v5.1 / PowerShell Core v7.3.4, which can serve as a starting point for about_Error_Handling, along with links to issues to related problems.
Do tell me if and where I got things wrong.
The sheer complexity of PowerShell's current error handling is problematic, though I do realize that making changes in this area is a serious backward-compatibility concern.
-
Types of errors:
-
True PowerShell errors:
-
Non-terminating errors are issued by cmdlets or functions to signal failure with respect to specific inputs while continuing to process further (pipeline) input by default.
-
This allows potentially long-running commands to run to completion, despite partial failure. The errors reported can then be inspected via the error records collected in automatic variable
$Error, allowing reprocessing of only the failed objects later (see below). -
Note, however, that is possible for all input objects to cause nonterminating errors, amounting to complete failure overall.
-
Most cmdlet errors are non-terminating errors; e.g.:
'/NoSuch', '/' | Get-Itemreports an error for non-existent path/NoSuch, but continues processing with valid path/.
-
-
Terminating errors:
- Important: In the context of remoting - whether explicit (e.g., via
Invoke-Command) or implicit (e.g., via modules using implicit remoting) - terminating errors (of either kind) are converted to non-terminating ones. - Types of terminating errors:
-
Script-terminating (fatal) errors:
- By default, they abort the entire enclosing script as well as any calling scripts. On the command line, a single statement or a list of statements submitted together can be thought of as an implicit script.
- Only a
try/catchorTrapstatement can prevent hat.
- Only a
- The only way to directly trigger such an error is with the
Throwkeyword. - Note: Script-terminating errors are in effect fatal errors from the perspective of your code, if unhandled, - see Cmdlet.ThrowTerminatingError() is not affected by
-ErrorActionPowerShell/PowerShell#14819 (comment) for a technical discussion.
- By default, they abort the entire enclosing script as well as any calling scripts. On the command line, a single statement or a list of statements submitted together can be thought of as an implicit script.
-
Statement-terminating errors are the (statement-level) counterparts to non-terminating errors: they terminate the enclosing statement (pipeline or expression) and are issued to signal that a statement encountered a problem that doesn't allow it to meaningfully start or continue processing.
- Important:
- Statement-terminating errors truly only terminate the statement at hand. By default, the enclosing script continues to run.
- The statement that is terminated is only the immediately enclosing statement; therefore, for instance, a statement-terminating error that occurs in the (
-Process) script block of aForEach-Objectcall does NOT terminate the pipeline as a whole - see below for an example.
- Situations in which statement-terminating errors occur:
- PowerShell's fundamental inability to even invoke a command generates a statement-terminating runtime error, namely:
- A non-existent command:
nosuch -l # no such command exists - A cmdlet or function call with invalid syntax, such as incorrect parameter names or missing or mistyped parameter values:
Get-Item -Foo # -Foo is an invalid parameter name
Select-Object -First notanumber # -First expects an [int]- Exception: If the mistyped value is bound via the pipeline, the resulting error is non-terminating see @alx9r's example.
- Attempting to call another script that fails to parse (that is syntactically invalid).
- A non-existent command:
- While rare, cmdlet invocations that succeed syntactically can themselves generate statement-terminating errors, such as based on the contents of parameter values (not caught by parameter validation) or the state of the environment.
- Strict-mode violations (such as trying to access a nonexistent variable when
Set-StrictMode-Version 1or higher is in effect). - Expressions can trigger statement-terminating errors too, namely:
-
via PowerShell expression runtime errors; e.g.:
1 / 0 -
via exceptions thrown by .NET method calls; e.g.:
[int]::Parse('foo') -
via statement-terminating errors that occurs inside a
(...)subexpression (but not inside$(...)or@(...), which are independent statement contexts); e.g.:(Get-Item -Foo) + 'hi'is a statement composed of a single expression that is terminated as a whole; by contrast,$(Get-Item -Foo) + 'hi'is a statement composed of an expression and an embedded statement, so only the$(...)part is terminated -+ 'hi'is still executed.- As an aside: Better names for operators
$()and@()would therefore have been [array] substatement operators rather than [array] subexpression operators.
- As an aside: Better names for operators
-
- PowerShell's fundamental inability to even invoke a command generates a statement-terminating runtime error, namely:
- Important:
-
- Important: In the context of remoting - whether explicit (e.g., via
-
-
Failures signaled by external utilities (command-line / console applications such as
findstr.exeon Windows and terminal-based utilities such asawkon Unix) via their exit codes are non-terminating - in fact, external utilities reporting nonzero exit codes are not errors in a PowerShell sense.- Note that the only standardized information that a utility's exit code carries is whether it succeeded overall (exit code
0) or failed overall (nonzero exit code), with no further distinction.- The exit code of the most recently executed external utility is stored in the automatic
$LASTEXITCODEvariable - see below.
- The exit code of the most recently executed external utility is stored in the automatic
- Stderr output from external utilities is NOT considered error output - see below.
- Note that the only standardized information that a utility's exit code carries is whether it succeeded overall (exit code
-
-
Default error actions, logging and success indicators:
-
Default actions when errors occur:
-
a non-terminating error:
- does not terminate the current statement (pipeline): processing continues with further input objects, if present.
- is output to PowerShell's error stream (stream number 2), which in the console prints in red with detailed context information; preference variable
$ErrorViewcan be used to change to a different format.- Note: The current default format is multi-line and "noisy", and there's a suggestion to change that: see Suggestion: Add an $ErrorView option that displays concise, single-line error information and consider making that the default - default formatting of ErrorRecord PowerShell/PowerShell#3647
- is logged in the automatic
$Errorcollection (see below)
-
a statement-terminating error:
- terminates the current statement, but the script continues processing.
- Important: The statement that is terminated is only the immediately enclosing statement; therefore, for instance, a statement-terminating error that occurs in the (
-Process) script block of aForEach-Objectcall does NOT terminate the pipeline as a whole - it only terminates the script-block invocation at hand.- Example:
1, 2 | ForEach-Object -Process { Get-Item -Foo } -End { 'pipeline ran to completion' }- The
Get-Item -Foocalls cause statement-terminating errors, but the statements they terminate are the instances of the-Processscript blocks, so pipeline processing continues, andpipeline ran to completionprints.
- The
- Example:
- Important: The statement that is terminated is only the immediately enclosing statement; therefore, for instance, a statement-terminating error that occurs in the (
- is output to PowerShell's error stream.
- is logged in the automatic
$Errorcollection.
- terminates the current statement, but the script continues processing.
-
a script-terminating error:
- terminates the entire script
- is output to PowerShell's error stream
- is logged in the automatic
$Errorcollection (which only matters if the session isn't terminated as a whole)
-
an external utility signaling failure by exit code / producing stderr output:
- does not terminate the current statement (pipeline).
- While stderr output is sent to PowerShell's error stream, it is not formatted like an error in the console, and by itself does not imply that an error occurred.
- Stderr output is NOT logged in the automatic
$Errorcollection
-
-
Default success indicators:
-
Automatic Boolean variable
$?reflects whether the most recent statement, including calls to external utilities, experienced any error:-
$True, if none, and$False, if at least one error occurred (because, in the case of non-terminating errors, multiple errors may have occurred). -
In short:
$?returning$Falsetells you only that some error occurred:- You cannot infer how many inputs experienced failure, which can range from 1 to all of them.
- You cannot infer (from $? alone) whether a terminating error occurred or not.
-
Caveat: Because
$?is set by every statement, its value is only meaningful immediately after the statement of interest. -
$?is NOT set / set as expected in the following cases:- If a (by definition terminating) error was handled with a
Try/CatchorTrapstatement - unless theCatchhandler is empty. (Non-terminating errors cannot be caught that way.)- With a
CatchorFinallyblock present, the success of whatever statement appears last inside of them, if any, determines what$?is set to, with a (non-empty)Finallyblock taking precedence.
- With a
- If the error is a non-terminating error and the command originating the error is enclosed in
(...)- which turns the command into an expression - it is then the expression's own success that is reflected in$?:
(Get-Item /NoSuch); $?yields$True, because the expression itself - whose sole purpose was to wrap the command - technically succeeded.
(Get-Item -Foo); $?yields$False, because the statement-terminating error terminated the expression as a whole.
- If a (by definition terminating) error was handled with a
-
-
When calling external utilities, automatic variable
$LASTEXITCODEcomplements$?by containing the specific exit code set by the most recently executed external utility.-
$?is set to$Trueif the utility's exit code was0, and to$Falseotherwise. -
Note that while
$?is only meaningful immediately after the statement of interest,$LASTEXITCODEremains relevant until the next external-utility call is made; either way, however, it is preferable to save the value in another variable if it needs to be inspected later.
-
-
Note: Currently, PowerShell lacks operators that allow chaining of commands based on whether they indicate success or not, such as the
&&and||control operators in Bash:
-
-
Default logging:
- Errors are logged in memory (for the duration of the session) in the global automatic
$Errorvariable in reverse chronological order (most recent error first; i.e.,$Error[0]refers to the most recent error):-
$Erroris a collection of type[System.Collections.ArrayList]and the errors are stored as error records of type[System.Management.Automation.ErrorRecord]that wrap the underlying .NET exception, which all errors ultimately are (instances of[System.Exception]or a derived type). -
To inspect
$Erroritems, pipe them toFormat-List -Force(direct output would result in the same format as when the error originally occurred); e.g.:$Error[0] | Format-List -Force -
You can clear the collection anytime with
$Error.Clear() -
Errors are NOT logged in the following circumstances:
-
When non-terminating errors occur in a cmdlet / advanced function to which
-ErrorAction Ignorewas passed (see below). -
What external utilities print to stderr is not considered error output, therefore it is NOT logged in
$Error(see below).
-
-
- Errors are logged in memory (for the duration of the session) in the global automatic
-
-
Modifying the default error actions and logging behavior:
-
PowerShell commands and expressions:
-
Via common parameter
-ErrorActionor preference variable$ErrorActionPreference:-
Per the documentation as of this writing, specifying an error action should only affect non-terminating errors.
- While this is true with respect to the
-ErrorActioncommon parameter, it is incorrect with respect to the$ErrorActionPreferencepreference variable, which affects terminating errors as well (see Clarify the intended behavior/documentation/terminology of terminating errors PowerShell/PowerShell#4292).- Caveat: a
try/catchstatement at a higher level can apparently preempt$ErrorActionPreference- see try/catch overrides $ErrorActionPreference? PowerShell/PowerShell#5693
- Caveat: a
- Arguably, the
-ErrorActioncommon parameter should affect terminating errors too, but changing that would be a major breaking change. - see Cmdlet.ThrowTerminatingError() is not affected by-ErrorActionPowerShell/PowerShell#14819 - In cmdlets (advanced scripts/functions) written in PowerShell code, an
-ErrorActionvalue gets automatically translated into a script/function-local$ErrorActionPreferencevalue, which with-ErrorAction SilentlyContinuecan result inthrowstatements getting ignored and unwanted code execution - see Throw outside Try\Trap acts like a non-terminating error PowerShell/PowerShell#19500 (comment)
- While this is true with respect to the
-
In the context of remoting, terminating errors (of either type) are converted to non-terminating ones.
- Caveat: When invoking functions from a module that uses implicit remoting, neither
-ErrorActionnor$ErrorActionPreferencework as expected:-ErrorActionis applied remotely, which means that whatever errors occur is invariably converted to a non-terminating error locally.- The caller's
$ErrorActionPreferencevalue (unless it happens to be the global variable value) is not seen by the implicitly remoting module (which is typically an auto-generated script module), because modules have their own namespaces that only inherit from the global scope. The current inability of a script module to opt into the caller's preferences (without cumbersome workarounds) is discussed here: Need straightforward way to honor the caller's preference-variable values / inherited common parameters in functions defined in script modules PowerShell/PowerShell#4568
- Caveat: When invoking functions from a module that uses implicit remoting, neither
-
The supported action values are:
Continue... non-terminating errors only: output errors and log them, but continue processing the current statement (non-terminating errors).Stop... non-terminating errors and statement-terminating errors via$ErrorActionPreferenceonly: escalate the error to a script-terminating one.SilentlyContinue... likeContinue, but silence error output, while still logging errors. Via$ErrorActionPreferenceonly, also applies to both types of terminating errors (processing continues).Ignore(-ErrorActionparameter only) ... non-terminating errors only: likeSilentlyContinue, but without logging errors in$Error. Due toIgnorenot being supported via$ErrorActionPreference, n/a to terminating errors.- Due to a bug, you can currently set
$ErrorActionPreferencetoIgnore, even though you shouldn't be able to: Preventing setting 'Ignore' as the $ErrorActionPreference value is poorly enforced on the command line, and not at all in scripts PowerShell/PowerShell#4348 - Due to another bug, passing
-ErrorAction Ignoreto an advanced function currently causes a spurious statement-terminating error - see -ErrorAction Ignore doesn't work for calling advanced functions PowerShell/PowerShell#1759
- Due to a bug, you can currently set
Inquire... prompt the user for the desired action, including the option to temporarily enter a nested session for debugging. Via$ErrorActionPreferenceonly, also applies to both types of terminating errors.Suspend(workflows only) ... automatically suspends a workflow job to allow for investigation.
-
Ad-hoc, when calling cmdlet/advanced functions, you can pass common parameter
-ErrorActionto modify the behavior of non-terminating errors (only!).- As stated, the
-ErrorActionparameter has no impact on terminating errors - in line with documented, but debatable behavior.
- As stated, the
-
Scope-wide (including descendant scopes, unless overridden there), you can set preference variable
$ErrorActionPreference, which sets the scope's default behavior for all occurrences of non-terminating behaviors, and - against documented behavior - also for terminating errors.-
Caveat: Advanced functions defined in script modules do not see the caller's preference variables by default, and making them do so is currently quite cumbersome - see Need straightforward way to honor the caller's preference-variable values / inherited common parameters in functions defined in script modules PowerShell/PowerShell#4568
-
As stated, cmdlets invoked via an implicit-remoting modules also do not see the caller's preference variables.
-
-
The
-ErrorActionparameter takes precedence over the$ErrorActionPreferencevariable. -
Non-terminating errors:
- may alternatively be silenced with
2>$null, analogous to silencing stderr output from external utilities (see below). Terminating errors ignore2>$null- Note that error output suppressed with
2>$nullor redirected to file with2>$fileis nonetheless still recorded in the$Errorcollection, by design - see Using a 2> error-stream redirection with cmdlets still records error records in $Error PowerShell/PowerShell#4572
- Note that error output suppressed with
- may additionally be collected command-scoped in a user variable, via common parameter
-ErrorVariable, unless-ErrorAction Ignoreis also used. Note that use of-ErrorVariabledoes not affect the error output behavior; e.g., with error actionContinuein effect, non-terminating errors still print to the console, whether or not you use-ErrorAction.
- may alternatively be silenced with
-
-
Catching terminating errors with
Try/CatchorTrapstatements:-
Important: Only terminating errors (of either type) can be caught this way, and
Try/CatchandTrapare effective irrespective of the current$ErrorActionPreferencevalue (and any-ErrorActioncommon parameter, which fundamentally only applies to non-terminating errors). -
Inside a
Catchblock:- Automatic variable
$_contains the[System.Management.Automation.ErrorRecord]instance representing the terminating error at hand. - You can use just
Throw(without an argument) to re-throw the error at hand.
- Automatic variable
-
You may define multiple
Catchblocks by applying optional filters based on the underlying .NET exception types - seeGet-Help about_Try_Catch_Finally. -
Error logging: Errors caught this way are still logged in the
$Errorcollection, but there is debate around that - see Why do handled exceptions show in ErrorVariable? PowerShell/PowerShell#3768
-
-
-
External utilities:
-
Given that an external utility signaling failure by returning a nonzero exit code is not a true PowerShell error:
-
A failure signaled this way cannot be escalated to a script-terminating error via the
$ErrorActionPreferencevariable. -
Error messages (printed to stderr) are formatted like regular output and are NOT logged in the automatic
$Errorcollection variable, nor do they affect how automatic variable$?is set (that is solely based the utility's exit code).- The rationale is that external utilities, unlike PowerShell commands, have only two output streams at their disposal: stdout for success (data) output, and stderr for everything else, and while error messages are printed to stderr, other non-data output (warnings, status information) is sent there too, so you cannot make the assumption that all stderr output represents errors.
-
-
However, you can capture stderr output for later inspection:
-
Using output redirection
2>allows you to suppress stderr output or send it to a file:
whoami invalidarg 2>$nullsuppresses stderr output;
whoami invalidarg 2>err.txtcaptures stderr output in fileerr.txt-
Caveat: Due to a bug still present as of PowerShell Core v6.0.0-beta.5, when you use any
2>redirection (redirection of PowerShell's error stream):-
Stderr lines are unexpectedly collected in the
$Errorcollection, despite the redirection, and even though stderr output is by default NOT collected in$Error. -
This can accidentally trigger a script-terminating error if
$ErrorActionPreference = 'Stop'happens to be in effect.
-
-
-
There is currently no convenient mechanism for collections stderr lines in a variable, but, as a workaround, you can use redirection
2>&1to merge stderr output into PowerShell's success output stream (interleaved with stdout output), which allows you to filter out the stderr messages later by type, because PowerShell wraps the stderr lines in[System.Management.Automation.ErrorRecord]instances; e.g.:$stdout, $stderr = ($(whoami; whoami invalidarg) 2>&1).Where({ $_ -isnot [System.Management.Automation.ErrorRecord] }, 'Split'); you can then convert the$stderrarray to strings with$stderr = $stderr | % ToString.- Suggestion: Add redirection syntax that allows in-memory collection of stderr lines output by external utilities, akin to the -ErrorVariable common parameter PowerShell/PowerShell#4332 proposes adding a feature for directly collecting stderr lines in a variable, using a syntax such as
2>&var
-
-
-
-
Reporting custom errors in functions and scripts:
-
Guidelines for when to use non-terminating errors vs. statement-terminating errors in the context of creating cmdlets are in MSDN topic Cmdlet Error Reporting; a pragmatic summary is in this Stack Overflow answer of mine.
-
Non-terminating errors:
- The
Write-Errorcmdlet is designed to generate non-terminating errors; e.g.:
Write-Error "Not an existing file: $_"wraps the message in a[System.Management.Automation.ErrorRecord]instance and outputs it to the error stream. - However,
Write-Errorcurrently neglects to set$?to$Falsein the caller's scope, the way a compiled cmdlet does when reporting a non-terminating error - see Write-Error in a function doesn't set automatic success variable $? to $False (doesn't set the execution status to indicate a non-terminating error) in the caller's scope PowerShell/PowerShell#3629- Thus, after calling a script or function,
$?may mistakenly contain$Trueeven whenWrite-Errorcalls were made. - The workaround - available in advanced functions only (those whose
param()block is decorated with the[CmdletBinding()]attribute) - is to use the$PSCmdlet.WriteError()method - see example below.
- Thus, after calling a script or function,
- The
-
Terminating errors:
-
Statement-terminating errors:
-
PowerShell has NO keyword- or cmdlet-based mechanism for generating a statement-terminating error.
-
The workaround - available in advanced functions only - is to use
$PSCmdlet.ThrowTerminatingError():- Note: Despite the similarity in name with the
Throwkeyword (statement), this truly only generates a statement-terminating error. - See example below.
- Note: Despite the similarity in name with the
-
On a side note: Sometimes it is desirable to terminate upstream cmdlets, without terminating the pipeline as a whole, the way
Select-Object -First <n>does, for instance. As of Windows PowerShell v5.1 / PowerShell Core v6.0.0-beta.5, there is no way to do this, but such a feature has been suggested here: Allow user code to stop a pipeline on demand / to terminate upstream cmdlets. PowerShell/PowerShell#3821
-
-
Script-terminating errors:
- Use the
Throwkeyword.- Any object can be thrown, including none. In the context of a
Catchhandler (as part of aTry/Catchstatement), not specifying an object causes the current exception to be re-thrown. - Unless the object thrown already is of type
[System.Management.Automation.ErrorRecord], PowerShell automatically wraps the object in an instance of that type and stores the object thrown in that instance's.TargetObjectproperty.
- Any object can be thrown, including none. In the context of a
- Use the
-
-
Example use of $PSCmdlet.WriteError() in an advanced function so as to create a non-terminating error (to work around the issue that Write-Error doesn't set$? to $False in the caller's context):
# PSv5+, using the static ::new() method to call the constructor.
# The specific error message and error category are sample values.
& { [CmdletBinding()] param() # quick mock-up of an advanced function
$PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
# The underlying .NET exception: if you pass a string, as here,
# PS creates a [System.Exception] instance.
"Couldn't process this object.",
$null, # error ID
[System.Management.Automation.ErrorCategory]::InvalidData, # error category
$null) # offending object
)
}
# PSv4-, using New-Object:
& { [CmdletBinding()] param() # quick mock-up of an advanced function
$PSCmdlet.WriteError((
New-Object System.Management.Automation.ErrorRecord "Couldn't process this object.",
$null,
([System.Management.Automation.ErrorCategory]::InvalidData),
$null
))
}Example use of $PSCmdlet.ThrowTerminatingError() to create a statement-terminating error:
# PSv5+, using the static ::new() method to call the constructor.
# The specific error message and error category are sample values.
& { [CmdletBinding()] param() # quick mock-up of an advanced function
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
# The underlying .NET exception: if you pass a string, as here,
# PS creates a [System.Exception] instance.
"Something went wrong; cannot continue pipeline",
$null, # error ID
[System.Management.Automation.ErrorCategory]::InvalidData, # error category
$null # offending object
)
)
}
# PSv4-, using New-Object:
& { [CmdletBinding()] param() # quick mock-up of an advanced function
$PSCmdlet.ThrowTerminatingError((
New-Object System.Management.Automation.ErrorRecord "Something went wrong; cannot continue pipeline",
$null, # a custom error ID (string)
([System.Management.Automation.ErrorCategory]::InvalidData), # the PS error category
$null # the target object (what object the error relates to)
))
}
Activity
SteveL-MSFT commentedon Aug 16, 2017
JuanPablo Jofre (@juanpablojofre) this would make a great about topic
zjalexander commentedon Oct 11, 2017
PowerShell/PowerShell#3629
nightroman commentedon Dec 20, 2017
A note about
ThrowTerminatingError, either regression from v2 or some not clear design change.In the following script,
ThrowTerminatingErroris not caught in v5 and v6. It is caught in v2.Copied from https://github.com/nightroman/PowerShellTraps/tree/master/Basic/ThrowTerminatingError/Catch-is-not-called
strawgate commentedon Jan 13, 2018
Michael Klement (@mklement0) I see your recommendation to utilize$PSCmdlet.WriteError() instead of write-error to fix the issue with $ ? but it looks like these have different behaviors when the "ErrorActionPreference" of the script/module is set to "stop".
Im testing this in Powershell 5.1
If erroractionpreference = stop and you use $PSCmdlet.WriteError() it appears to produce a locally non-terminating error but when it leaves the function it appears to be terminating. That error is not catch-able in the function producing the error but is catche-able by the caller.
If erroractionpreference = stop and you use write-error it appears to produce a statement terminating error which can be caught by the function.
Running Test-WriteError will terminate the script:
Running Test-Write-Error will not terminate the script:
In other words, $PSCmdlet.WriteError does not appear to throw a terminating error when $ErrorActionPreference = Stop whereas write-error does
mklement0 commentedon Jan 21, 2018
Bill Easton (@strawgate): Thanks for pointing out that difference - I have no explanation for it.
To me, your discovery suggests that one currently should always use
$PSCmdlet.WriteError()instead ofWrite-Error(which, needless to say, is very unfortunate):While you could argue that technically
Write-Errorexhibits the correct behavior - withStopin effect, writing a non-terminating error should throw an exception - pragmatically speaking, the$PSCmdlet.WriteError()behavior is much more sensible:You don't want to catch your own attempts with
try/catchinside an advanced function to report a non-terminating error.Instead, you want the caller to handle this, as implied by the calling context's
$ErrorActionPreferencevalue or the-ErrorActionvalue passed to the advanced function (which function-internally is translated into a local-scope$ErrorActionPreferencevariable reflecting that value).There is no good solution to this problem with
Write-Error:if you used
Write-Error -ErrorAction Continue, your owncatchhandler wouldn't get triggered, but execution would continue, which is undesired.The only solution is to reissue the
Write-Errorfrom thecatchblock, which is obviously cumbersome and awkward.alx9r commentedon Feb 3, 2018
Michael Klement (@mklement0) Thank you very much for your excellent post. It has been invaluable to me for making sense of a number of things.
Script Terminating Errors vs Exceptions
Is there a difference between "script-terminating errors" and exceptions? There seems to be places in the PowerShell project that distinguish between "script-terminating errors" and exceptions. I haven't, however, been able to observe a difference between them. Is there a difference?
What's the "Script" in "Script-Terminating Error"?
What is "script" meant to refer to in "script-terminating errors"? It seems like "script-terminating errors" often (almost always in my use of PowerShell) do something other than terminate a script. It seems like the only case where the stack unwinding caused by a "script-terminating error" stops at something that would be called a "script" is where there happens to be nothing that catches the "script-terminating error". Am I missing something? Is there some other definition of "script" that applies to "Script-Terminating Error"?
.ThrowTerminatingError does not a Statement-Terminating Error Make
The original post includes the following statements:
Consider, however, that
outputs
outer catch. If it were true that "statement-terminating errors truly only terminate the statement at hand" and.ThrowTerminatingError()caused a statement-terminating error, it seems to me that the output would have beenstatement following statement with .ThrowTerminatingError.mklement0 commentedon Feb 7, 2018
alx9r: Thanks for the nice feedback and digging deeper. I'll need more time to give your post the attention it deserves, but let me say up front that it was I who came up with the term "script-terminating", out of necessity, given that the existing docs made no distinction between the sub-types of terminating errors.
Thus, there's nothing authoritative about this term, and if it turns out to be a misnomer, we should change it.
mklement0 commentedon Feb 17, 2018
alx9r:
Disclaimer: Little of what I state below has been arrived at through source-code analysis. All readers are welcome to point out any misinformation.
From what I understand, all PowerShell errors (as opposed to stderr output from external utilities) are exceptions under the hood.
A script-terminating error is simply an exception that isn't caught by PowerShell.
Presumably, the
more technically accurate term would be runspace-terminating error, but the term runspace is not a term a PowerShell(-only) user is necessarily expected to be familiar with. [Update: It's hard to come up with a succinct, technically accurate term, though calling such errors fatal is a pragmatic alternative - for the technical underpinnings, see PowerShell/PowerShell#14819 (comment)]In practice, what I call a script-terminating error, when uncaught:
aborts a running script, including its callers.
aborts a runspace created with
[powershell]::Create().aborts a single command line submitted at the command prompt - even if composed of multiple,
;-separated statements. (Presumably, such a command line runs in a runspace created ad hoc).Again, the most descriptive term is open to debate.
Session-terminating error is perhaps an alternative, but that also could be confusing with respect to behavior at the command prompt.
In this context, the pipeline as a whole is the statement, even though a pipeline is itself by definition composed of sub-statements (an expression or command as the 1st segment, and commands as the subsequent segments).
Again, the terminology is open to debate: If an actual pipeline is involved, you could argue that pipeline-terminating error is the better choice, but note that that doesn't apply to expression-only statements such as
1 / 0that also generate a statement-level-only error - it is for that reason that I stuck with statement as the qualifier.Another example to demonstrate the pipeline behavior of a statement-terminating error:
The above yields:
That is, the entire pipeline was instantly terminated when the statement-terminating error was generated (no further input objects were processed, and the
endblocks didn't run).alx9r commentedon Feb 25, 2018
Since I came across this post months ago I've been trying to arrive at empirical proof of the various statements in the OP about "terminating", "non-terminating", "script-terminating", and "statement-terminating" errors. Currently I am doubtful that there exists definitions of those terms that would result in a useful taxonomy of PowerShell errors.
I have found few useful generalizations about PowerShell errors that stand up to testing. Most hypotheses involving generalizations and nuance (including some in OP) can be disproven with a small amount of testing. The truth about PowerShell errors seems remarkably resistant to simplicity.
I am fairly confident, however, that the following generalizations about PowerShell errors are true:
a. write records to the
$global:Errorvariableb. write records to a non-global copy of
$Errorc. write records to a variable named in an
-ErrorVariableargumentd. output to PowerShell's error stream
e. update the value of
$?f. terminate a pipeline
g. terminate a statement
h. throw an exception
a. the value of
$ErrorActionPreferenceb. the value passed to
-ErrorActionor the value of$ErrorActionc. the presence of an enclosing
try{}block (eg. How, exactly, does PowerShell decide whether .ThrowTerminatingError() terminates only the statement? PowerShell/PowerShell#6098)d. the precise way an error is reported (eg.
Write-Errorvs.$PSCmdlet.WriteError()vs.throw$PSCmdlet.ThrowTerminatingError())More nuanced generalizations would, of course, be useful. Currently, however, I am skeptical that such generalizations can be found and proven. Indeed my attempts at doing so have seemed sisyphean.
From my perspective the useful takeaway from this exercise is currently the following:
mklement0 commentedon Feb 25, 2018
alx9r:
My writing the OP had multiple intents:
I wanted to point that the current documentation is hopelessly inadequate.
I wanted to provide a pragmatic summary of the de-facto behavior, so as to help us poor souls who have to live with and make some sense of the current behavior.
In the course of doing so, I coined terms that made sense to me where none were to be found in the docs.
While I don't doubt that my summary doesn't cover all nuances - it was arrived at empirically as well, not through studying source code - my hope was that it covers most aspects and gets at least the fundamentals right.
Believe me, I understand that you ran out of steam trying while investigating, but if you have pointers as to where my summary is fundamentally off, please share them.
I wanted to start a conversion about cleaning up the byzantine mess that PowerShell error handling currently is - save for linking to various issues highlighting specific problematic behaviors, I didn't do a good job of making that explicit, however.
32 remaining items
johndog commentedon Jul 23, 2021
Strict modes don't seem to have been discussed, but they introduce yet another nuance into the domain.
Here's one dichotomy...
First, context:
Then this shows that using an undefined variable generates an error but doesn't stop execution:
But the same script block run with Invoke() generates an exception, not just an error:
It would have been much nicer of course if strict mode always produced an exception, regardless of ErrorActionPreference.
mklement0 commentedon Jul 24, 2021
johndog, good idea to cover strict-mode violations too; I've just updated the initial post.
The short of it is that strict-mode violations result in statement-terminating errors by default.
This applies to both your examples, which only differ in the scope of what constitutes the statement being terminated:
In
& {$nonExistentVariable;"got here"},$nonExistentVariableis a statement by itself, therefore"got here"still executes (withContinueorSilentlyContinuein effect).In
{$nonExistentVariable;"got here"}.Invoke()the scope of the statement is the method call (as an aside: from PowerShell code, there's usually no reason to call.Invoke()), so that& { {$nonExistentVariable;"won't get here"}.Invoke(); "got here" }would again still execute"got here"..Invoke(), both flavors of terminating errors inside the script block cause the method to fail with a statement-terminating error - even if the triggering error was a script-terminating one; try& { {throw 'a fit'; "won't get here"}.Invoke(); "got here" }Note that while PowerShell's error handling is built on .NET exceptions (in the case of the two flavors of terminating errors), it's better to discuss it in terms of non-terminating vs. statement-terminating vs. script-terminating errors.
davidtrevor23 commentedon Oct 12, 2023
Trying to wrap my head around pipeline execution and statement-terminating behavior. From all the posts and comments, I gathered the understanding that the following two methods can both be used to provoke a statement-terminating behavior.
But in my tests they do not behave the same. The first example continues the pipeline, whereas the second terminates the pipeline. Can someone explain where my understanding is wrong?
On another note, can someone point me to a resource that explains how exactly objects get processed when passed through the pipeline operator (in what order). I do not quite understand the order of operation in this comment:
#1583 (comment)
mklement0 commentedon Oct 12, 2023
$PSCmdlet.ThrowTerminatingError()by design terminates the enclosing advanced function/script.Thus,
1 / 0inPROCESS { 1 / $i }terminates just1 / $iand continues execution (even inside the script block; placing something after would still execute, e.g.PROCESS { 1 / $i ; 'after' }A better comparison of the two scenarios would be:
Now both errors terminate the entire statement (which happens to be a pipeline in this example, but it equally applies to an expression) and there is no success output. However the next statement (
'after') executes.Perhaps the following addresses your second question:
Note that something like
1, (1 / 0), 2is an array-construction expression, which means that all elements are evaluated up front. The same applies to a pipeline or expression enclosed in(...)Thus, the statement-terminating error occurs right away and terminates the entire pipeline.
By contrast, if you use
$(...),@(...)or& { ... }with separate statements (separated with;or just newlines), it is again just the statement that triggers the error alone that is terminated:1and2get to print, because they're separate statements;(1 / 0)and(0 | calc)only terminate themselves.Note: With
$(...)and@(...)the error message prints first, because these two operators collect all success output first, whereas& { ... }streams its output.ci: Fix pwsh scripts not exiting on error
denis-komarov commentedon Oct 10, 2024
Michael Klement (@mklement0)
Thank you for such a fundamental and in-depth description of error handling mechanisms. Just for the sake of completeness, it seems that another separate class of errors could be mentioned. This is "Fatal error. Internal CLR error" against which even try catch is powerless. However, I am not sure of the practical value of this addition.
alx9r commentedon Nov 6, 2024
Origin Location of Errors from Explicitly-Invoked Scriptblocks
The resulting error objects that arise in a PowerShell
catch{}block contain location information for the actual source of the error that is, at best, elusive. The contents of the error object are affected by at least the following:Write-Error,throw, or some lower-level exception like1/0., the call operator&,ForEach-Object,Scriptblock.InvokeConsider files containing scriptblocks according to the following table:
{ 1/0 }{$PSCommandPath | Get-Item | % BaseName | Write-Error -ErrorAction Stop}{throw $($PSCommandPath | Get-Item | % BaseName)}By invoking each of those scriptblocks using one of the calling methods inside a
try{}, the resultant error object can be caught and examined. The object tree of each such an error object can be traversed to find any mentions of the file where the scriptblock is defined. The paths of possible mentions in these object trees is numerous, and the prevalence of most such mentions across the matrix of call methods and error causes is sparse. There are, however, four paths that together contain mention of the originating scriptblock for all of these combinations of error and calling method. Those paths are summarized in the following table:ScriptStackTrace
InvocationInfo.
ScriptName
Exception.
InnerException.
ErrorRecord.
ScriptStackTrace
Exception.
ErrorRecord.
InvocationInfo.
ScriptName
This suggests the following:
ForEach-Objectin particular will either correctly show the file where the error arose or not depending the cause of the error and whether only$.InvocationInfo.ScriptNameis shown. (PowerShell 7 shows the correct location when usingForEach-Object, whereas Windows PowerShell does not, for example.)ForEach-Objecterror_action_stopscenario from somewhere other than what is contained by the caught error object. The method I'm using to traverse the error object only finds mention of the correct file in$.ScriptStackTrace.$ErrorView='NormalView'shows the location of the call site not the offending scriptblock forerror_action_stopcalled with.or&but shows does show the offending scriptblock using forerror_action_stopwithForEach-Object.$.ScriptStackTraceprobably should be shown for.and&forerror_action_stopin$ErrorView='NormalView'instead. The same seems to apply to all errors arising fromScriptblock.Invoke()regardless of their cause.Add about_Error_Handling and fix error terminology across docs
Add about_Error_Handling and fix error terminology across docs
SufficientDaikon commentedon Mar 27, 2026
Submitted a PR to address this: #12890
What it includes:
about_Error_Handlingreference topic documenting all three error categories (non-terminating, statement-terminating, script-terminating)about_CommonParametersandabout_Preference_Variablesthat-ErrorAction/$ErrorActionPreferencehave "no effect on terminating errors"about_Try_Catch_Finally,about_Throw,about_Trap, andeverything-about-exceptionsto use precise terminologyabout_Calculated_Properties,about_Pipeline_Chain_Operators, andabout_PwshAll behavior claims verified against the engine source (
MshCommandRuntime.WriteError,ThrowTerminatingError,ActionPreferenceStopExceptionescalation path). Every code example tested in pwsh 7.x.Add about_Error_Handling and fix error terminology across docs
Add about_Error_Handling and fix error terminology across docs (Micro…