Why is `Install-Module` not respecting $ErrorActionPreference=Stop from the script scope?

I have the following script:

$ErrorActionPreference = "Stop"
Install-Module 'DoesNotExist' -Force
Write-Host "This should not be printed!"

I’d assumed, that if an error occurs in Install-Module, it terminates, because I set $ErrorActionPreference to Stop. Unfortunately it doesn’t.

What even makes it weirder is, that if I set $ErrorActionPreference in the global scope to Stop, it works.

$global:ErrorActionPreference = "Stop"

You’re seeing an unfortunate design limitation, discussed in GitHub issue #4568:

Problem:

Commands implemented as PowerShell functions that originate in modules:

  • do not see any preference variables set in the caller’s scope

  • except if the calling scope happens to be the global one – which is typically not the case.

Unfortunately, there are no good solutions to this problem, only cumbersome workarounds:

Workaround for callers:

A non-global caller, such as a script or a function (which run in a child scope by default) must:

  • Either: Explicitly set preference variables in the global scope (e.g., $global:ErrorActionPreference="Stop")

    • So as not to affect subsequently executing code, be sure to restore their original values afterwards, which – for robustness – must be done in the finally clause of a try / catch / finally statement.
  • Or: On a call-by-call basis, use the corresponding common parameters (e.g. -ErrorAction Stop)

    • The challenge with -ErrorAction Stop is that it is not fully equivalent to $global:ErrorActionPreference="Stop" in that only acts on non-terminating errors, and not also on statement-terminating ones, additionally necessitating a try / catch or a trap statement. See this answer for background information.

These workarounds are spelled out in detail in the middle section of this closely related answer.

Generally, the challenge is to know when a workaround is even needed, as a given command’s name doesn’t reveal whether it is PowerShell-implemented and whether it comes from a module.
You can use the following test for a given command; if it returns $true, the workaround is needed:

# -> $true, 
# because Install-Module is from a module and implemented as a PowerShell function.
& {
  $cmd = Get-Command $args[0] 
  $cmd.CommandType -eq 'Function' -and $cmd.ModuleName 
} Install-Module

Workaround for module authors:

Dave Wyatt has authored a helper module, PreferenceVariables, whose Get-CallerPreference function can be used inside module functions to obtain an outside caller’s preference variables.

In other words: You can use this to overcome the design limitation and make your PowerShell-implemented cmdlet (advanced function) behave like binary cmdlets with respect to the caller’s preference variables.[1]

Get-CallerPreference use is explained in this blog post.

Note: You don’t strictly need another module to implement this functionality, it – which doesn’t require much code – could be integrated directly into a module.


[1] There are other, subtle differences, discussed in this answer.

Leave a Comment