2

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?

Sonali Gupta
  • 494
  • 1
  • 5
  • 20
  • I'm unable to reproduce the unexpected behavior you describe. How are you executing the statements? Are you pasting them into a terminal? If so, the host application might be simply executing them as two separate statements... – Mathias R. Jessen Jun 29 '22 at 11:34
  • I can reproduce it with v5.1. with a .ps1 file. My `$ErrorActionPreference` is `Continue`. – Dávid Laczkó Jun 29 '22 at 11:36
  • What makes you think the cmdlet would produce a terminating error? You need to set your error preference to `Stop` for that. At most it would produce an error that would stop the pipeline. – Santiago Squarzon Jun 29 '22 at 11:50
  • 1
    Because in the first example it falls into the catch block without setting any ErrorActionPreference. ALso, ErrorActionPreference by default is continue which implies(acc to me) by first case it should be terminating. and try/catch doesn't capture non-terminating – Sonali Gupta Jun 29 '22 at 11:53
  • @SantiagoSquarzon The about_Try_Catch_Finally docs talk only about terminating errors. – Dávid Laczkó Jun 29 '22 at 11:54
  • @SonaliGupta the fact that the script "continues" or is not halting after the error implies that the error is only terminating for that pipeline not for the script itself. A terminating error would stop the script no matter what your Error Preference is (which is not something you would see often for a cmdlet) – Santiago Squarzon Jun 29 '22 at 12:31
  • 1
    @mklement0 I agree re the docs not giving the full scope of the details. – Santiago Squarzon Jun 29 '22 at 13:06

2 Answers2

2

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.

Confusingly, the seeming per-command equivalent, namely passing -ErrorAction Stop to an individual cmdlet, does not promote a statement-terminating error to a script-terminating one - you still need try / catch or trap for that - see below.

The upshot:

  • To treat all errors occurring in your scope - including non-terminating ones - as fatal, use $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:

    • Scope-wide, you can use the (rarely used) 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.'
  • Alternatively, especially if you need you more fine-grained control, use 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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • So between command terminating exception versus non-terminating exception, the main difference(benefit) would be that we can try/catch the first but not the second without setting the ErrorActionPreference. Rest both of them allow a script to move on and hence arent that different? – Sonali Gupta Jun 30 '22 at 09:35
  • 1
    @SonaliGupta, note that .NET exceptions only underly the _terminating_ errors in PowerShell, so calling non-terminating errors exception isn't strictly correct). Non-terminating errors are reported for individual input objects from the pipeline, and processing continues with further input objects. Unlike terminating errors, you can suppress non-terminating errors with `-ErrorAction Ignore` or `2>$null`, and you can _collect_ them with `-ErrorVariable`. More details about how non-terminating vs. terminating errors differ are in [this answer](https://stackoverflow.com/a/39949027/45375). – mklement0 Jun 30 '22 at 14:37
1

I think either the documentation of $ErrorActionPreference or the error classification of PS is wrong, but when I have Continue then I can reproduce the above, but setting it to Stop, "after terminating" is no longer popping up (tested for v5.1. with a .ps1 file).

Dávid Laczkó
  • 1,091
  • 2
  • 6
  • 25
  • 1
    What the docs don't mention: there are _statement_-terminating and there are _script_-terminating errors. Cmdlets emit only the former (though more typically emit _non_-terminating errors). `$ErrorActionPreference = 'Stop'` promotes both statement-terminating and non-terminating errors to _script_-terminating ones. See the comments on the question for more information. – mklement0 Jun 29 '22 at 12:35