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
traportry / catchor 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
throwstatement. - The preference variable
$ErrorActionPreferencesets the behaviour for both terminating and non terminating errors, and the-ErrorActioncommon parameter sets it for a single cmdlet/ Advanced-function - Problems arise when functions assume they will only be run with
$ErrorActionPreferenceset to particular values, or assume that they will only see errors which have been generated in particular ways.