This self-answered question, written as of PowerShell (Core) v7.3.x, aims to address the following symptoms:
Sometimes, neither using -ErrorAction -Stop nor $ErrorActionPreference = 'Stop' appears to be effective for being able to catch and handle an error via a try / catch statement.
Here are two examples :
Get-NetAdapter ignores $ErrorActionPreference = 'Stop' from a script or function (from a scope other than the global one):
# Run on Windows. Affects both PowerShell editions.
# The `catch` block isn't triggered, and the (still non-terminating) error prints.
& { # Simulate running in a child scope.
$ErrorActionPreference = 'Stop'
try {
Get-NetAdapter nosuch
}
catch {
"Should get here, but don't."
}
}
-ErrorAction Stop (Get-NetAdapter nosuch -ErrorAction Stop) does work as expected.[This turned out to be a bug in preview versions of PowerShell (Core) v7.4 only] Get-AppLockerFileInformation ignores both a non-global $ErrorActionPreference = 'Stop' and use of ErrorAction Stop; only the latter is shown here:
# Run on Windows. Affects PowerShell (Core) v7+ only
# The `catch` block isn't triggered, and the (still non-terminating) error prints.
try {
Get-AppLockerFileInformation NoSuch.exe -ErrorAction Stop
}
catch {
"Should get here, but don't in PowerShell (Core)."
}
So the questions are:
What determines when -ErrorAction and/or $ErrorActionPreference are ineffective?
What workarounds are available when they aren't?
Two related factors determine whether -ErrorAction and/or $ErrorActionPreference are ineffective when calling a given command:
Fundamentally, due to how PowerShell's scoping works, commands hosted in a script module (i.e. one implemented in PowerShell, as opposed to via a compiled .NET assembly) do not see the caller's preference variables, including $ErrorActionPreference, except when calling from the global scope, which is not the case when you call from a (non-dot-sourced) script or function.
This - very unfortunate - design limitation is the subject of GitHub issue #4568. Said issue also mentions the PreferenceVariables module, which script-module authors can use to work around the limitation (see this blog post for details).
For callers, using -ErrorAction Stop, i.e. the corresponding common parameter, on a per-call basis, is honored, but isn't fully equivalent, because it doesn't also apply to statement-terminating errors and therefore additionally explicitly requires converting the latter to script-terminating (fatal) ones using try / catch with throw or an equivalent trap statement - see below for details and an alternative workaround via (temporarily) setting $ErrorActionPreference globally.
Modules that use implicit remoting - which are by definition script modules too - may introduce an additional complication:
IMPORTANT: The following either no longer applies to Windows PowerShell 5.1 / PowerShell (Core) 7 or may only affect select modules - I don't know which; I have old notes suggesting that implicitly remoting Exchange cmdlets were affected in Windows PowerShell 5; do tell us if you have more information.
With commands from such modules, even -ErrorAction Stop is categorically ineffective, because this parameter is propagated to the remotely executing command, and - by design - remotely occurring terminating errors translate locally into non-terminating errors.
When targeting multiple remote machines, such as via Invoke-Command's -ComputerName parameter, a terminating error occurring remotely should not terminate the multi-machine operation overall, therefore such remotely terminating errors are translated into locally non-terminating ones.
[NO LONGER TRUE IN GENERAL] This logic applies to scenarios with implicit remoting too - despite the fact that by definition only one remote machine is being targeted.
While using implicit remoting usually requires explicit action - such as the use of the Import-PSSession cmdlet - there is one scenario where implicit remoting is automatically used by default:
In PowerShell (Core) 7+, if you try to use commands from a module that is compatible with Windows PowerShell only, the associated module is by default automatically imported via the Windows PowerShell Compatibility feature, which involves communicating via a hidden powershell.exe child process.
While this isn't technically remoting, given that everything happens on the local machine, the underlying cross-process communication mechanism is the same as in true remoting, which - as an aside - also reduces type fidelity - see this answer.
The only way to turn such remotely occurring, invariably non-terminating errors into terminating ones that can be caught with try / catch is to (temporarily) set the global incarnation of the $ErrorActionPreference variable to 'Stop', as shown below.
Workarounds:
Either: On a per-call basis, use -ErrorAction Stop rather than $ErrorActionPreference = 'Stop', for full robustness in combination with either try / catch or trap to ensure that (the relatively rare) statement-terminating errors too are converted to script-terminating (fatal) ones:
try {
Get-NetAdapter nosuch -ErrorAction Stop
}
catch {
Write-Verbose -Verbose "OK, caught: $_"
# If desired, re-throw to make script-terminating (fatal)
throw
}
# ------------------------
# ALTERNATIVE, via `trap`:
trap {
Write-Verbose -Verbose "OK, caught: $_"
# If desired, use `break` to ensure that the error is script-terminating (fatal).
# Use `continue` to quietly continue.
break
}
Get-NetAdapter nosuch -ErrorAction Stop
Note:
Unfortunately, $ErrorActionPreference = 'Stop' and -ErrorAction Stop aren't created equal: the latter only acts on non-terminating errors, whereas the former acts on statement-terminating errors too - see this answer. However, in the context of using try / catch and trap, this distinction doesn't matter, because a statement-terminating error by itself also triggers the catch block / trap script block.
If there is still a problem with modules that use implicit remoting, you'll need the next workaround instead.
Or: Temporarily set $global:ErrorActionPreference = 'Stop', i.e. set the preference variable globally.
& { # Simulate running in a child scope.
$prevGlobalPref = $global:ErrorActionPreference
$global:ErrorActionPreference = 'Stop'
try {
Get-AppLockerFileInformation NoSuch.exe
}
catch {
Write-Verbose -Verbose "OK, caught: $_"
# If desired, re-throw to make script-terminating (fatal)
throw
} finally {
$global:ErrorActionPreference = $prevGlobalPref
}
}
As for how to tell whether a given command is from either module type (regular vs. implicitly remoting):
The following helper function, Get-CommandModuleType, tells you what type of module a given command is from, with output 'Script' indicating a regular (non-implicitly-remoting) script module, and 'ImplicitRemoting' an implicitly remoting one; see the comment-based help in the source code for details.
Sample calls, from PowerShell (Core) on Windows:
# -> 'Script'
Get-CommandModuleType Get-NetAdapter
# -> 'ImplicitRemoting'
Get-CommandModuleType Get-AppLockerFileInformation
function Get-CommandModuleType {
<#
.SYNOPSIS
Indicates a given command's module type.
.DESCRIPTION
Aliases are automatically resolved to their underlying commands.
Note:
* The output is a *string* that is one of the following:
* '(None)', If the command is a function *not from a module*.
* 'ImplicitRemoting' for an implicitly remoting module.
* 'Script' for regular script modules (that don't use implicit remoting)
* 'Binary' if the command is a *binary cmdlet* - even if its part of
a module that is a *script* module, due to *also* containing
PowerShell code. Strictly speaking, binary cmdlets are themselves
modules.
* Not all script modules report themselves as such via the underlying
module's manifest; some of them report just 'Manifest'. Conversely,
a script module can also contain *binary* cmdlets.
To see the actual value from the module definition, use the -Raw switch.
.EXAMPLE
Get-CommandModuleType Get-NetAdapter
Reports the *de facto* type of the Get-NetAdapter command.
.EXAMPLE
Get-CommandModuleType Get-NetAdapter -Raw
Reports the *formal* type of the module that hosts the Get-NetAdapter, as
specified in its manifest.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $CommandName,
[switch] $Raw
)
$command = Get-Command $CommandName
if ($command -and $command.ResolvedCommand) {
$command = $command.ResolvedCommand
Write-Verbose "Resolved command is: $command"
}
if ($module = $command.Module) {
Write-Verbose "Source module is: $module; command type is $($command.CommandType)"
if (-not $Raw -and $command.CommandType -in 'Function', 'Filter') {
# A function from a script module
if ($module.PrivateData.ImplicitRemoting) {
# an implicitly remoting module
'ImplicitRemoting'
}
else {
'Script'
}
}
elseif (-not $Raw -and $command.CommandType -eq 'Cmdlet') {
'Binary'
}
else {
if ($Raw) { Write-Verbose 'Reporting raw module type, i.e as manifested.' }
$module.ModuleType.ToString()
}
}
else {
'(None)'
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With