I was expecting that with a terminating exception, the script execution will hault in powershell.
Since try/catch will only capture terminating exception(other than ErroActionPreference etc) I had the below code
try {
$hashtableJson = "asdfg.kjhgf" | ConvertFrom-Json
} catch {
Write-Host "inside catch block"
}
and I can see "inside catch block" printed.
Which means the exception System.ArgumentException
which comes due to invalid json is terminating.
Now my doubt is in the below script
$hashtableJson = "asdfg.kjhgf" | ConvertFrom-Json
Write-Host "after terminating"
In the above script I expected the run to break on the first line itself but the after terminating
still gets printed.
Any explanation why its not exiting the flow? or how to better figure out if exception is terminating or not?
PowerShell has two types of terminating errors, which the documentation currently conflates, unfortunately:
Statement-terminating errors, as reported by compiled cmdlets (as opposed to written-in-PowerShell advanced functions)[1] and .NET methods, which only terminate the current statement by default; that is, by default execution of the script continues, with the next statement.
Script-terminating (runspace-terminating, fatal) errors, as raised by throw
and when $ErrorActionPreference = 'Stop'
is in effect or when -ErrorAction Stop
is passed to cmdlets and advanced functions / scripts and a non-terminating error occurs.
The third error type - the most common one - is a non-terminating error, which doesn't affect the control flow at all: by default even processing of the current statement (with remaining pipeline input) continues. As for when cmdlets are expected to use non-terminating vs. terminating errors, see this answer.
Note:
Arguably, there should only ever have been one type of terminating error: script-terminating (fatal), which, if needed, could be handled as merely statement-terminating, with try
/ catch
(see below), but it's too late to change that.
For a comprehensive overview of PowerShell's surprisingly complex error handling, see this GitHub docs issue.
ConvertFrom-Json
, as a compiled cmdlet, emits a statement-terminating error when invalid JSON is provided as input, which explains why execution of your script continues.
You can use try
/ catch
or trap
to catch both statement- and script-terminating errors - but not non-terminating ones by default, unless you promote them to script-terminating ones with -ErrorAction Stop
.
By contrast, $ErrorActionPreference = 'Stop'
promotes all errors to script-terminating (fatal) ones, including non-terminating ones.
Note:
In the absence of try
/ catch
and trap
, passing -ErrorAction Stop
to an individual cmdlet does not promote a statement-terminating error to a script-terminating one, which means that execution continues with the next statement in that case.
This confusing asymmetry - $ErrorActionPreference = 'Stop'
acting on all errors to make them fatal vs. -ErrorAction Stop
only acting on non-terminating errors - is the subject of GitHub issue #14819.
The upshot:
$ErrorActionPreference = 'Stop'
# Treat *all* errors in this scope (and descendant scopes)
# as *script*-terminating (fatal).
$ErrorActionPreference = 'Stop'
# Trigger a *non*-terminating error, which is *also* promoted
# to a script-terminating one.
Get-Item NoSuchFile
# Trigger a *statement*-terminating error, which is promoted
# to a script-terminating one.
# Note: Because of the Get-Item error, this statement isn't reached,
# Comment it out to see that this statement too causes a
# script-terminating error.
$hashtableJson = "asdfg.kjhgf" | ConvertFrom-Json
'This statement is now never reached.'
By contrast, use the following techniques to make only terminating errors fatal while keeping non-terminating errors non-terminating:
trap
statement to that effect, namely by placing trap { break }
in your scope:# Trap both kinds of *terminating* errors and abort.
trap { break }
# Trigger a *non*-terminating error, which is left alone (not
# caught by the trap).
Get-Item NoSuchFile
# Trigger a *statement*-terminating error, which triggers the trap.
$hashtableJson = "asdfg.kjhgf" | ConvertFrom-Json
'This statement is now never reached.'
try
/ catch
around statements of interest and use throw
from the catch
block to trigger a script-terminating error.try {
# Trigger a *non*-terminating error, which is left alone (does
# not trigger the catch block).
Get-Item NoSuchFile
# Trigger a *statement*-terminating error, which triggers the catch block.
$hashtableJson = "asdfg.kjhgf" | ConvertFrom-Json
} catch {
throw # Re-throw the *statement*-terminating error as *script*-terminating.
}
'This statement is now never reached.'
[1] It is possible to make an advanced function / script emit a statement-terminating error, namely via $PSCmdlet.ThrowTerminatingError()
, but that is both obscure and cumbersome; in practice, it is far more common to use the script-terminating throw
statement.