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:
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:
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.
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.
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
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.
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
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:
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
).
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.
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:
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”:
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:
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
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.
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
ortry / 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.