The 'definitive' Terminating and Non-Terminating errors in PowerShell.

In something else I’m working on, I started to write

There are half a dozen kinds of exceptions in PowerShell, and they are usually called “terminating and non-terminating errors.”

That wasn’t a proof-reading mistake!
I’ve read a lot things which glide over the many “ifs , buts, and excepts”, needed to explain why a PowerShell command might run after one error, but not another, or why some attempts to control messages don’t work as expected. Or why this is a terminating error, and that is also a terminating error but they act differently. Or even what “terminating” means technically and practically in PowerShell, or the role of error handling is.
These aren’t just in random writings on the net either - the examples in this post show a couple of things don’t do what PowerShell’s help says they should… Usage of “error” and “exception” has grown to treating the two as synonyms, and I’m not going try to undo that, although I have some sympathy for people who will read something and say “That’s not an error, it’s an exception!” or vice versa.

In this post I’ve set out to collect and illustrate the different situations - setting myself up as the one totally complete, 100% accurate source would mean I’m bound to miss something crucial - hence the quotes around ‘definitive’ in the title. But the piece has become a bit of an epic.

1 Abnormal end – “abend” or Goodnight Vienna from an external program

Some systems use “Abend” (“evening” if you speak German) for a system crash. I remember Novell NetWare used it instead of “panic” or “blue screen of death”. In other places, it’s used (as here) for any “didn’t run to successful completion”.
The following two lines give the same result in a PowerShell Script, a Unix shell script, or a steam-driven batch file:

    ping -z localhost  
    ping -z www.github.com

When the first ping runs it sees -z as an invalid switch, prints a message, sets an exit-code to say things ended badly and quits.
Control returns to the script that called it, which runs the next command: the first ping has no way to prevent it.
Whatever calls ping can check the exit-code (sometimes called ErrorLevel) before continuing, but most scripts leave most things unchecked.
Generally, zero on exit indicates success, but non-zero values may be informational rather than indicating a failure. On Linux, ping writes to standard error to say “-z” is not valid, but on Windows ping writes the error message to standard output (other commands, like nslookup write to standard error on both OSes). PowerShell provides two automatic variables, $LASTEXITCODE to get the exit-code of the last external program or script, and the Boolean $? to answer “was the last command error-free?”; but if the external command is wrapped in try / catch the catch will not be triggered by $? holding false or by messages going to standard error.

For the rest of the post the focus is within PowerShell; I mentioned try / catch, which treats the whole try block as one, the catch block can’t return execution to the next step, the older (but now little used) trap statement can do that.

2 Just a warning

When something is not processed as expected, but the command can still get to the desired conclusion, that is the place for PowerShell to send a warning. Again this isn’t (normally) caught by try / catch: it is just a message sent to a specific output stream.

3 Like a warning, but worse

If you run Get-Item "NonExistentName", the cmdlet reports that it can’t process the item, which could, almost, be a warning, but it doesn’t pass the test of getting to the right conclusion, so it is classified as an error. (Attempting to delete a non-existent item would be a more arguable case, but the boundaries between informational, warning, and error can be fuzzy).
Get-Item reports the error by writing an object to the error stream (where 2 used the warning stream), but that doesn’t mean that it must exit.

Get-Item "NonExistentName", "." will produce an error and then get the current directory; if this is in the middle of a pipeline, the rest of the pipeline will run normally. So this:

    "NonExistent", "." | Get-Item | % -Process {$_.name} -End {"Finish"}
    

will output the error message for the first item, then the name of the second item, and finally run the end block and output “Finish”.

As with the external program, $? indicates “error free”. This pipeline completed with errors so $? is false, but because it is an internal command there is no exit code and $LASTEXITCODE is unchanged.

Internally, Get-Item calls the equivalent of $PSCmdlet.WriteError() and passes it an exception object which contains the message, information to classify the error, and contextual information about where it occurred. PowerShell’s Write-Error cmdlet acts a wrapper for that method, and can build the object, given just the message text. When objects arrive in the error stream, PowerShell adds them to the $error automatic variable and applies appropriate formatting to display them (“appropriate” varies - PowerShell 7 has a concise view which Windows PowerShell 5 doesn’t).
Like the previous examples, by default try / catch doesn’t see anything to do when an object is written to the error stream: the two following examples will print the original error without running their catch block:

    try {Write-Error "Oh dear" } catch {"Caught"}
    
    try {Get-Item "NonExistent"} catch {"Caught"}
    

Parameters and preference variables can tell PowerShell to adopt non-default behaviours when things are written to the error, warning, debug and verbose streams. Changing $ErrorActionPreference would change the behaviour of the two lines above.
The rest of this post concentrates on the -ErrorAction parameter and its associated preference variable, but -WarningAction would have the same effect in many places.

4 “I can’t go on”, “That’s just you - I can”

The WriteError() method discussed in the previous section is available to PowerShell cmdlets (including advanced functions), but some internal errors in PowerShell don’t come from commands. For example. 2/0 will say that you can’t divide by zero, or [regex]::new("(") will report that an unclosed bracket won’t parse. These are operations which can’t continue - terminating errors and try / catch is there to handle them. This contrasts with messages sent with Write-Error() which are non-terminating by default and ignored by try / catch.

Outside of scripting languages, something reporting “I needed to terminate” means whatever called it must terminate in the same way unless it can handle the error; whatever called that must handle or terminate and so on.
But the very first example in this post showed that scripting languages don’t stop when there is an error in an external command, and the default approach to all errors is the same. Like the first example, the following lines would illustrate the point in a Unix shell script, a Windows (or DOS!) batch file or PowerShell Script.

    eco "Hello!"
    echo " 'eco' was a typing error."
    

All three will say “there is no such command as eco” and then run the echo command.

Hopefully, some code will clarify what happens in different situations; I’ll start with 4 functions named one, two, three and four.
Function four runs a pipeline: one | two | three and will do so in slightly different ways for different examples.
Function one puts some data into the pipeline, two processes it and demonstrates different types of errors, three receives the output from two and all 4 functions print progress information. Depending on what happens in function two, and on how four runs the pipeline we may see pipeline code run in one, two and three (or not), and we may see post-pipeline code run in four (or not).
I recommend copying and pasting the code into a PowerShell session to do additional experiments.

    function one {[cmdletbinding()]param()
        begin {Write-Host "one begins"}   end {Write-host "one ends"}
        process {1,2,3}
    }
    function two {param ([parameter(ValueFromPipeline)]$p)
        begin {Write-Host "two begins"}   end {Write-host "two ends"}
        process {
            if ($p -eq 2) {$p / 0 } # cause a division by zero error

            $p
        }
    }
    function three {param ([parameter(ValueFromPipeline)]$p)
        begin {Write-Host "three begins"} end {Write-host "three ends"}
        process {
            Write-host "three Processed $p"
            $p
        }
    }
    function four {[cmdletbinding()]param()
        begin {Write-Host "four begins"}  end {Write-host "four ends"}
        process {
            $result = one | two | three
            Write-host "And the result is "
            $result
        }
    }
    

Note: To make the code visible without scrolling, some lines have been combined in a way they wouldn’t be in typical code and I’ve omitted [cmdletbinding()] in two and three where [param()] attributes mean it is assumed - that way it all fits on one screen.

Here is the result of running four with the first version of the functions:

    ps> four
 
    four begins
    one begins
    two begins
    three begins
    three Processed 1
        RuntimeException:
        Line |
           4 |      if ($p -eq 2) {$p / 0 }  # cause a division by zero error
             |                     ~~~~~~
             | Attempted to divide by zero.
    three Processed 2
    three Processed 3
    one ends
    two ends
    three ends
    And the result is
    1
    2
    3
    four ends

The begin blocks run, the value 1 is is processed, then the error occurs. The message is displayed, and the very next command - function two outputting 2 - runs as if no error had happened; the pipeline and the function which called it both continue normally, with three and four receiving 2 and any subsequent value(s). All the end blocks run to complete the process. Later I’ll refer back to this “write message and carry on” behaviour as “a”.

This behaviour might look like a non-terminating error, but in fact PowerShell is printing the message from a terminating error and treating it as handled. The error can be handled with try / catch and the first variation is to change function four to show that.

    function four {[cmdletbinding()]param()
        begin {Write-Host "four begins"}  end {Write-host "four ends"}
        process {   # Add try {} Catch {}

                try { $result = one | two | three} catch {Write-Host "Error in the pipeline"}
                Write-host "And the result is "
                $result
        }
    }
    

Now the result of running four is quite different:

    ps> four

    four begins
    one begins
    two begins
    three begins
    three Processed 1
    Error in the pipeline
    And the result is 
    four ends

Catching the error meant that the message “Error in the pipeline” was printed. I said above that “the whole try block runs as one”, so we shouldn’t expect the pipeline to resume, and it doesn’t.
The statement in the try block said assign the pipeline result(s) to a variable, so, although function three processed the first value, nothing will go into $Result until the pipeline has completed. Non-completion means that no assignment happens and function four has no values to output. Running

    ps> try {  one | two | three} catch {Write-Host "Error in the pipeline"}  
    

doesn’t queue the output to go into a variable so it is displayed.
Function four continues normally after handling the error, but the pipeline terminated, so the functions in the pipeline do not run their end blocks.

I’ll label ths behaviour “b” - the calling function did not stop but the pipeline did.

Try really tries

The example above shows that try catches a terminating error in something which it calls - and this can be be multiple calls deep

    function five {param ($p) two  $P}
    function six  {param ($p) five $P}
    try {six 2} catch {"error happened"}
    
    two begins
    error happened

So the try block calls six, six calls five, five calls two, an error occurs in two and it is caught.

Changing the error action

If the pipeline command is run with -ErrorAction Stop (or ErrorActionPreference has been set to stop in any other way), that can also produce behaviour “b”, abandoning the pipeline, and continuing from the next line in the function that called it. But we will see different results of -ErrorAction Stop later.

    function four {[cmdletbinding()]param()
        begin {Write-Host "four begins"}  end {Write-host "four ends"}
        process { #remove try {} / catch and use -ErrorAction Stop

                Write-Host "Error action preference in four is $errorActionPreference"
                $result = one | two -ErrorAction Stop | three  
                Write-host "And the result is "
                $result
        }
    }
    

The inclusion of the error message is the only change to see between the version above setting error action and the version before it using try / catch. The division-by-zero error itself can’t change, PowerShell is changing the way it is handled inside function two. We can see the effect with the updated version of four:

    ps> four

    four begins
    Error action preference in four is Continue
    one begins
    two begins
    three begins
    three Processed 1
    two: 
    Line |
       5 |                  $result = one | two -ErrorAction Stop | three
         |                                  ~~~~~~~~~~~~~~~~~~~~~
         | Attempted to divide by zero.
    And the result is
    four ends

The error is now shown as “the call to function two returned a divide-by-zero error”. When two continued it was shown as “there was a divide-by-zero error at this line.”

Function four prints the message, considers it handled, and continues because that is what the local error action preference set as the default for terminating errors - it imposed a different terminating-error behaviour (stop) on two
Running four with -ErrorAction SilentlyContinue changes what it does when two sends back an error:

    ps> four -ErrorAction SilentlyContinue

    four begins
    Error action preference in four is SilentlyContinue
    one begins
    two begins
    three begins
    three Processed 1
    And the result is
    four ends

The pipeline end blocks did not run, nor did 2 or 3 get processed by function three, so we can see that Stop still applied to two in the pipeline but when the error came back to four, it was handled silently and execution continued.
And if we run four -errorAction Stop, four doesn’t run its final steps, like this:

    ps> four -ErrorAction Stop`

    four begins
    Error action preference in four is Stop
    one begins
    two begins
    three begins
    three Processed 1
    two:
    Line |
       5 |                  $result = one | two -ErrorAction Stop | three
         |                                  ~~~~~~~~~~~~~~~~~~~~~
         | Attempted to divide by zero.
    

This is a new behaviour, which I’ll call “c” - it stopped the pipeline, and it stopped the calling function, because, that’s what the error action in the calling function said it should do for a terminating error.

Catch me if you can…

In 2 we saw that Get-Item can continue after an Item Not Found error, it just writes something to the error channel. But in other cases cmdlets must terminate in the same way that a mathematical operation must after a divide-by-zero.
I’m working with the preview of PowerShell 7.3 where Start-Sleep can be given a [TimeSpan] to wait; but a time-span can be years and the wait-time in milliseconds is a signed 32-bit integer setting a limit of about 25 days. The source code on github shows (at line 126 in the version I’m looking at) that Start-Sleep calls ThrowTerminatingError() which we see’ll in (4). This gives three different things to test in try / catch

    try {2/0}                                      catch {"Bad calculation"}
    try {Start-Sleep ([timespan]::FromDays(30)) }  catch {"Bad timespan"}    # not in 7.2.x or earlier

    try {Get-Item "Nonexistent" }                  catch {"Bad item"}
    

The code above gives the following:

    Bad calculation    
    Bad timespan
    Get-Item: Cannot find path ...

The item not found error from Get-Item is is just a non-terminating message to the error channel (by default) so it is non-catchable. So, if Get-Item is in the try block, or in something that is called from try block, (like function two in these examples) the catch won’t run. But the behaviour can be changed, like this:

    try {Get-Item "Nonexistent" -ErrorAction Stop} catch {"Bad item"}
    Bad item

The call made by Get-Item to say the error occurred doesn’t change, but that call acts differently when Error Action is set to Stop (either using a parameter, or by setting $ErrorActionPreference). You might think it makes the item not found error act like a over-long TimeSpan or dividing by zero - but terminating errors can have have different behaviours…

The next example changes function four back to its original form. Originally, function two continued at the line after the divide-by-zero, and the pipeline completed all its stages if error action was continue. Swapping the divide-by-zero to the bad Start-Sleep command gives the same result.
The next example modifies function two to use Get-Item -ErrorAction Stop instead (the effect on Get-Item would be the same if $ErrorActionPreference were set to Stop or the pipeline contained two -ErrorAction Stop).

    function two {param ([parameter(ValueFromPipeline)]$p )
        begin {Write-Host "two begins"}   end {Write-host "two ends"}
        process { # change the cause of the error

            if ($p -eq 2) {Get-Item "NonExistent" -ErrorAction Stop}
            $p
        }
    }
    function four {[cmdletbinding()]param()
        begin {Write-Host "four begins"}  end {Write-host "four ends"}
        process { # back to no error action or try/catch

            Write-host "Error action preference in four is $ErrorActionPreference"
            $result = one | two | three
            Write-host "And the result is "
            $result
        }
    }
    

Running four with this version shows the effect of -ErrorAction stop on writing what would otherwise be a non-terminating error:

    ps> four

    four begins
    Error action preference in four is Continue
    one begins
    two begins
    three begins
    three Processed 1
    Get-Item: 
    Line |
       4 |              if ($P -eq 2) {Get-Item "NonExistent" -ErrorAction Stop}
         |                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         | Cannot find path ....
    

This is behaviour “c” again: stopping both the pipeline and the calling function. But there is a key difference here. The error action preference says that four should continue on a terminating error, yet it stops. I said above that changing error action didn’t make a non-terminating item not found error act like dividing by zero. If it did, then the default error action preference of continue would mean that this
Get-Item "NonExistent" -ErrorAction Stop; Write-host "boo"
would change Get-item’s error to a terminating one which would then be treated as handled by printing it, meaning execution would continue into Write-Host. In other words -Stop wouldn’t change the outward behaviour.
So things don’t work that way: to ensure that Stop means STOP, when a WriteError() converts the error type because of the Error Action Preference, the resulting error ignores Error Action Preference values. If we run this version of four with -ErrorAction SilentlyContinue / Continue / Ignore they have no effect.

I said above that it doesn’t matter where Error Action is set to Stop. Changing four back to its configuration before the last change - where running two -ErrorAction Stop gave behaviour “b” with a terminating error caused by divide-by-zero - will yield behaviour “c” with an error from Get-Item that is upgraded to a stopping one. The following code produces the same output as the code above.

    function two {param ([parameter(ValueFromPipeline)]$p )
        begin {Write-Host "two begins"}   end {Write-host "two ends"}
        process { # remove -ErrorAction stop 

            if ($p -eq 2) {Get-Item "NonExistent"}
            $p
        }
    }
    function four {[cmdletbinding()]param()
        begin {Write-Host "four begins"}  end {Write-host "four ends"}
        process { #  use -ErrorAction Stop

                Write-host "Error action preference in four is $ErrorActionPreference"
                $result = one | two -ErrorAction Stop | three  
                Write-host "And the result is "
                $result
        }
    }

5 Stop the pipeline, C# style - pipeline terminating errors

The functions one two and three interact in the pipeline: function one does some work, then functions two and three, before one does some more. This means PowerShell needs a mechanism for a fatal error in part of the pipeline to halt other cmdlets or functions, because simply writing an error and returning from two won’t stop one sending further data.

I referred earlier to compiled cmdlets calling ThrowTerminatingError(), which stops the cmdlet and any pipeline it is part of. In advanced functions, $PSCmdlet gives access to ThrowTerminatingError(), the next two examples show it stopping just the function it is in, and stopping a pipeline:

    function seven {[cmdletbinding()]param()
        begin {Write-Host "seven begins"}  end {Write-host "seven ends"}
        process { # create an error record, which contains an exception, then throw it.

            $ex = [System.Exception]::new("This is not the result you are looking for")
            $er = [System.Management.Automation.ErrorRecord]::new(
                $ex, "42", [System.Management.Automation.ErrorCategory]::OperationStopped, $null) 
            $PSCmdlet.ThrowTerminatingError($er)
        }
    }
    function four {[cmdletbinding()]param()
        begin {Write-Host "four begins"}  end {Write-host "four ends"}
        process { # back to no error action or try/catch

            Write-host "Error action preference in four is $ErrorActionPreference"
            $result = one | two | three
            Write-host "And the result is "
            $result
        }
    }
    function two { param ([parameter(ValueFromPipeline)]$p )
        begin {Write-Host "two begins"}   end {Write-host "two ends"}
        process {
            if ($P -eq 2) {seven}
            $p
        }
    }
    

The code above defines a new function named seven which is called in function two - two is called from the original version of four.
When $PSCmdlet.ThrowTerminatingError() is called, only seven is affected - everything runs except for seven's end block, as seen below:

    ps> four

    four begins
    Error action preference in four is Continue
    one begins
    two begins
    three begins
    three Processed 1
    seven begins
    seven: 
    Line |
       4 |              if ($P -eq 2) {seven}
         |                             ~~~~
         | This is not the result you are looking for
    three Processed 2
    three Processed 3
    one ends
    two ends
    three ends
    And the result is 
    1
    2
    3
    four ends

This is behaviour “a” (everything runs). Inside function two the error “This is not the result you are looking for” could be handled with try / catch or trap but, as in the divide-by-zero case, printing the messages is deemed to be sufficient handling when these aren’t used and error action preference is continue.

Modifying function two to contain ThrowTerminatingError() - instead of calling something which contains it - usually results in pipeline ending behaviour “b”:

    function two { param( [parameter(ValueFromPipeline)]$p )
        begin {Write-Host "two begins"}   end {Write-host "two ends"}
        process {
            if ($p -eq 2) {
                $ex = [System.Exception]::new("A different error")
                $er = [System.Management.Automation.ErrorRecord]::new($ex, "42",
                    [System.Management.Automation.ErrorCategory]::OperationStopped, $null)
                $PSCmdlet.ThrowTerminatingError($er)
                }
                $p
            }
        }
    

The pipeline terminates, and again a try/catch or trap would handle the error, and as before, printing the message is deemed to be sufficient handling, so four isn’t stopped.

    ps> four

    four begins
    Error action preference in four is Continue
    one begins
    two begins
    three begins
    three Processed 1
    two: 
    Line |
       5 |              $result = one | two | three
         |                              ~~~
         | A different error
    And the result is
    four ends
    

The examples so far have shown that if anything except Stop changing the behaviour of WriteError() leads to a terminating error, then when that error is received, if there is no try / catch or trap to handle it, the behaviour is determined by local value of error action and default setting of continue makes the printing of the error message sufficient “handling”.
But there is one major case we haven’t reached yet and I hope you are asking
Isn’t PowerShell’s throw statement the same as the ThrowTerminatingError() method?

6 Throwing spanners in the works

The answer to the previous question is that the terminating errors thrown in the two ways are different. The throw statement doesn’t need any arguments, but it can take an [ErrorRecord] object or take a string and convert it into an error record - the next example replaces the method in the previous one with the throw statement and sends an error record:

    function two { param( [parameter(ValueFromPipeline)]$p )
        begin {Write-Host "two begins"}   end {Write-host "two ends"}
        process {
            if ($p -eq 2) {
                $ex = [System.Exception]::new("A different error")
                $er = [System.Management.Automation.ErrorRecord]::new($ex, "42",
                    [System.Management.Automation.ErrorCategory]::OperationStopped, $null)
                throw $er
            }
            $p
        }
    }
    

Before this change, the method stopped the pipeline but not its caller (a.k.a behaviour “b”) because printing the error was sufficient handling in function four. But now that the throw statement is used in something called from function four, the behaviour changes to “c” - stopping both pipeline and caller - printing the message is not enough.

    ps> four

    four begins
    Error action preference in four is Continue
    one begins
    two begins
    three begins
    three Processed 1
    OperationStopped:
    Line |
       8 |                  throw $er
         |                  ~~~~~~~~~
         | A different error

A bit like upgrading WriteError(), if throw just sent a standard terminating error, the default error action preference of continue would mean the error was considered to be handled by printing the message, so throw wouldn’t stop anything. Instead, errors from the throw statement treat Continue as stop. By default, the block containing the statement stops, passing the error to its caller, if the error is not handled there that block stops, passing the error… and so on until either the error is handled or the whole stack is unwound, which is what happened in the example above. But passing the error up can mean it is processed with a different error action preference. We can see this by re-using function five from earlier

    function five {[cmdletbinding()] param ($p) 
        Write-host "Error action preference in five is $ErrorActionPreference"
        Two  $P -ErrorAction continue ; 
        "Five done"
    }
    
    ps> five 2
    
    Error action preference in five is Continue
    two begins
    OperationStopped:
    Line |
       8 |                  throw $er
         |                  ~~~~~~~~~
         | A different error


    ps> five 2 -ErrorAction SilentlyContinue
    
    Error action preference in five is SilentlyContinue
    two begins
    Five done
        

In the first of the two examples above five stopped because there was an unhandled error from throw, that error has a special handling of continue = stop so we get behaviour “c”.
In the second example, error action preference in two and five where different, so two stopped but when the error was handed up to five silentlyContinue took effect and five finished - behaviour b.
So: errors from the throw statement have special behaviour, but it only applies to continue where WriteError() switching to a terminating error also affected SilentlyContinue and Ignore

You might wonder why I set the error action explicitly when calling two from five in those two examples. There are three places to set the Error action.

    function five {[cmdletbinding()] param ($P)  two $P -ErrorAction continue ; "Five Done"} 
    five  
or
    function five {[cmdletbinding()] param ($P)  two $P; "Five Done"}
    Five -ErrorAction stop
or
    function five { [cmdletBinding{}] param($P)  two $p "Five Done"}
    $ErrorActionPreference = "Stop"
    $five

In the second and third two inherits the Error Action Preference set in five - which can have side effects and -ErrorAction can cause other unexpected results.
Get-Item "NonExistent","." -ErrorAction SilentlyContinue
Outputs the items it can get, and the messages written to the error channel are silenced.
But ThrowTerminatingError() doesn’t write messages to the error channel in the same way, and so
seven -ErrorAction SilentlyContinue
still outputs the error message.
This can be frustrating, I remember complaining that “Active directory cmdlets don’t respect error action”. On closer inspection, the commands are terminating when it is impossible for them to continue, and that style of termination can be handled with try / catch even if the catch block is empty to have the same effect as SilentlyContinue. Functions which upgrade WriteError() also need try / catch

The -ErrorAction parameter does not affect function seven, because it doesn’t receive terminating errors from anything it calls, and it doesn’t call WriteError() - but it does affect whatever calls seven. Setting the global preference $ErrorActionPreference = "SilentlyContinue" has no effect on seven’s call to $PSCmdlet.ThrowTerminatingError() which still exits the function sending an error back, but whatever receives that error continues silently.

When the pipeline was told to run
one | two -ErrorAction Stop | three
the ErrorAction parameter set the value $ErrorActionPreference inside two to jointly say two things: “In your scope the action for unhandled terminating errors (like divide-by-zero) is stop and if anything called from you writes an error message, make a terminating error which ignores Continue / SilentlyContinue and Ignore.
On one occasion above, function two was told this with divide-by-zero - giving behaviour “b” when the ‘normal’ terminating error was processed under a preference of continue. On another occasion it was told this with Get-Item writing an error - resulting in behaviour “c” with the ‘special’ terminating error.

This brings us back to WriteError() combined with Stop producing errors which can’t be silenced, where errors from the throw statement can be. Some people see this as a design flaw, but fixing it would create problems for existing scripts.
The last version of two uses the throw statement, and error action (even when inherited) can change what throw does. If the author of two assumes that use of throw means the function will always send back an error, the result of the next command might be a surprise.

    ps> $x = one | two -ErrorAction SilentlyContinue | three`  
 
    one begins
    two begins
    three begins
    three Processed 1
    three Processed 2
    three Processed 3
    one ends
    two ends
    three ends
    

Setting SilentlyContinue means the throw statement has no effect - if throw is there to prevent execution continuing into something which might be harmful, it is a good idea to use throw... ; return so if the command is told to silently continue, execution still goes back to whatever called it.

In the same way, code which assumes that it wil be able to run something after an error has occurred may not clean up properly if it is run with -ErrorAction Stop.

7 Break - which breaks stuff, and exit

For completeness the break statement is designed for flow control statements where it means “stop processing this item, and don’t process any further items”. And the PowerShell help has a warning not to use it elsewhere because:

PowerShell looks up the call stack for an enclosing construct. If it can’t find [one] the current runspace is quietly terminated. [meaning functions and scripts ] can inadvertently terminate their callers.

The throw statement terminates callers by default, but provides a message to say why and can it be handled with try / catch or trap or some values of $ErrorActionPreference. By contrast break doesn’t explain itself - and is either inside a construct which handles it or stops everything.
To see this we can create a function which contains a break statement and use it with the foreach statement -which handles break, and the ForEach-Object cmdlet - which doesn’t.

    function eight {param ($p) $p ; break }  

    foreach ($x in @(1..3)) { foreach ($y in @('a','b')) { eight "$x $y"  }} ; "Finished"

     1..3 | ForEach-Object -Process { "$_ a","$_ b" | ForEach-Object { eight $_ } } -End {"Ending"} ; "Finished"
    

The version using the foreach statement outputs

    1 a
    2 a
    3 a
    Finished

So the break in the inner loop causes it to exit after processing the value for “a” but the outer loop continues with item 2 and 3, and “Finished” is printed.

The second version with the cmdlet self-destructs when it it reaches break and only outputs 1 a.
If eight is a private function which will only be used in a foreach or similar statement this design might be useful, but if eight is available anywhere using break is reckless.

Exit is the only PowerShell statement I can think of which behaves differently in a script file to a function. (Spoiling my usual description that a script is a script block in a file, and a function is a script block in special-purpose variable.)

I’ve written about exit (and break) before. The exit statement takes an optional value for the exit code, which defaults to zero, and can be a negative number on Windows, but must be positive on Linux. If a script ends with exit 123 then running the script from within PowerShell will leave the 123 in $LastExitCode. If PowerShell is run with -File script.ps1, it exits with an exit code of 123. But if it is run with -Command script.ps1 or -command "& 'path with spaces.ps1'" then it runs the script, has nothing else to run and exits with an error code of 0 indicating “all commands ran”.

Exit 123 at the PowerShell prompt closes that instance of PowerShell returning an exit code of 123, as does Exit 123 in a function - so in a function it is “close PowerShell”, in a script it is “return with an exit code”. And that’s where this post started, so time for me to exit.

Finally {}

A summary

  • We have errors - which divide into non-terminating and terminating - warnings and exits.
  • Non-terminating errors can printed, silenced, drop into the debugger or ask the user what to do next; they can also be upgraded to terminating errors but without doing that, they cannot be trapped or caught.
  • Terminating errors can be handled with trap or try / catch or treated as “handled” just by printing them, handled silently, drop into the debugger or ask the user what to do next, or simply stop.
  • Some terminating errors disallow some options - in particular those from upgraded non-terminating errors and those generated by the throw statement.
  • The preference variable $ErrorActionPreference sets the behaviour for both terminating and non terminating errors, and the -ErrorAction common parameter sets it for a single cmdlet/ Advanced-function
  • Problems arise when functions assume they will only be run with $ErrorActionPreference set to particular values, or assume that they will only see errors which have been generated in particular ways.

<<Previous post       Next Post>>