Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are -ErrorAction Stop / $ErrorActionPreference = 'Stop' sometimes ineffective, e.g. with implicit remoting / Windows PowerShell Compatibility?

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."
      }
    }
    
    • However, use of -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)."
    }
    
    • See GitHub issue #20209

So the questions are:

  • What determines when -ErrorAction and/or $ErrorActionPreference are ineffective?

  • What workarounds are available when they aren't?

like image 426
mklement0 Avatar asked Apr 29 '26 17:04

mklement0


1 Answers

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.

      • I believe the design rationale behind this to be the following, related to PowerShell remoting in general:
        • 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)'  
    }
}
like image 68
mklement0 Avatar answered May 01 '26 08:05

mklement0