powershellpowershell-5.0

Powershell: terminating exception still continues to next command


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?


Solution

  • PowerShell has two types of terminating errors, which the documentation currently conflates, unfortunately:

    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:


    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:

    The upshot:

    # 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.'
    
    # 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 {
    
      # 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.