Why does this happen in the first place and why is the exception so cryptic?
The behavior you're seeing appears to be a bug (still present as of PowerShell 7.0; see this GitHub issue):
Calling exit
from a script method attached to a [pscustomobject]
instance - whether the script method is indirectly attached via New-Module -AsCustomObject
or directly via
Add-Member -MemberType ScriptMethod
- unexpectedly fails with the cryptic error you saw: instead of exiting, the error is emitted as a statement-terminating error (see below) and execution continues by default.
Methods defined on a PowerShell 5+ class
are not affected.
What is the canonical way of doing this correctly? I do want to exit and stop all further execution, not resume control flow. I also need to be able to return an exit code.
Typically, exit
is called from the top-level scope of a script file, not from inside a function (whether or not inside a module).
Note that exit
either immediately exits the enclosing script, or, if the function is called directly in an interactive session, exists the whole session.
If your quux()
method is called from within a script, and you indeed want to exit the enclosing script instantly, the only direct workaround in PowerShell 4- is not to try to emulate a PowerShell class and to define and call the quux
function directly.
However, an indirect workaround is to add a minor tweak to the throw
approach from Martin Brandl's helpful answer:
The solution is complicated by PowerShell's inconsistent use of terminating errors, which fall into two categories:
statement-terminating errors, which (by default) terminate the executing statement only, and then continue execution.
script-terminating errors (fatal errors), which terminate execution overall (a script and all its callers, and, in the case of a call to PowerShell's CLI, the PowerShell process as a whole).
This surprising distinction and PowerShell's surprisingly complex error handling in general are described in this GitHub docs issue.
A script method attached to a [pscustomobject]
instance, as used in your dynamic-module approach, is only capable of generating a statement-terminating error, not a script-terminating one.
Again, PowerShell 5+ class
methods behave differently: they create script-terminating errors.
To achieve overall termination while also reporting an exit code[1], you additionally need to do the following in the caller's scope:
- catch / trap the statement-terminating error.
- issue an
exit $n
statement in response (which in the context of the script scope will succeed), where $n
is the desired exit code.
You have two options:
Add a trap
statement to the caller's scope, which fires for both statement- and script-terminating errors and calls exit $n
, where $n
is the desired exit code.
To do so, the only tweak to Martin's answer is to replace trap { exit }
with, say,
trap { exit 1 }
to report exit code 1
.
Communicating the specific exit code from the throw
ing function takes extra work - see below.
Note that trap
is rarely used, but it enables a straightforward solution in your case.
Alternatively, use a try
/ catch
statement around the method call or a group of statements that could throw an error, and call exit
in the catch
handler:
try { $foo.quux() } catch { exit 1 }
Workaround that sets a specific exit code:
# Create a custom object with methods,
# via a dynamic module.
$foo = New-Module -AsCustomObject {
function bar
{
"bar"
}
function quux
{
# Throw a statement-terminating error with the desired exit code,
# to be handled by the `trap` statement in the caller's scope.
throw 5
}
}
# Set up the trap statement for any terminating error.
# Note: Normally, you'd place the `trap` statement at the
# beginning of the script.
trap {
# Get the exit code from the most recent error or default to 1.
# Note: $_ is a *wrapper* error record arund the original error record
# created with `throw`; however, the wrapper's .ToString() representation
# is the (string representation of) the original object thrown.
if (-not ($exitCode = $_.ToString() -as [int])) { $exitCode = 1 }
exit $exitCode
}
$foo.bar()
$foo.quux() # This triggers the trap.
# Getting here means that the script will exit with exit code 0
# (assuming no other terminating error occurs in the remaining code).
[1] If a script automatically terminates due to a script-terminating error, as a class
-based solution would generate, its exit code is invariably reported as 1
(and the error message prints), but only if the script was called via PowerShell's CLI (powershell -file ...
or powershell -commmand ...
); by contrast, inside an interactive PowerShell session $LASTEXITCODE
is not set in this case (but the error message prints).
Therefore, even if a PS 5+ solution is an option, you may still want to trap the script-terminating error and explicitly translate it into an exit
call with a specific exit code, as shown next.