Skip to content

Need straightforward way to honor the caller's preference-variable values / inherited common parameters in functions defined in script modules #4568

Open

Description

This is a longstanding issue that Dave Wyatt (@dlwyatt) explained in detail in this 2014 blog post, and he has even published module PreferenceVariables with advanced function Get-CallerPreference to ease the pain, via a hard-coded list of preference variable names.

In short: A script module's functions do not see the preference-variable values set in the caller's context (except if that context happens to be the global one), which means that the caller's preferences are not honored.

Update: Since implicitly setting preference variables is PowerShell's method for propagating (inheriting) common parameters such as -WhatIf, such parameters are ultimately not honored in calls to script-module functions when passed via an advanced function - see #3106, #6556, and #6342. In short: the common-parameter inheritance mechanism is fundamentally broken for advanced functions across module scopes, which also affects standard modules such as NetSecurity and Microsoft.PowerShell.Archive .

  • From a user's perspective this is (a) surprising and (b), once understood, inconvenient.
    Additionally, given that compiled cmdlets do not have the same problem, it is not easy to tell in advance which cmdlets / advanced functions are affected. In a similar vein, compiled cmdlets proxied via implicitly remoting modules also do not honor the caller's preferences.

  • From a developer's perspective, it is (a) not easy to keep the problem in mind, and (b) addressing the problem requires a workaround that is currently quite cumbersome, exacerbated by currently not having a programmatic way identify all preference variables in order to copy their values to the callee's namespace - see Improving the discoverability of PowerShell variables #4394.

A simple demonstration of the problem:

# Define an in-memory module with a single function Foo() that provokes
# a non-terminating error.
$null = New-Module {
  function Foo {
    [CmdletBinding()] param()
    # Provoke a non-terminating error.
    Get-Item /Nosuch
  }
}

# Set $ErrorActionPreference in the *script* context:
# Request that errors be silenced.
# (Note: Only if you defined this in the *global* context would the module see it.)
$ErrorActionPreference = 'SilentlyContinue'

# Because the module in which Foo() is defined doesn't see this script's
# variables, it cannot honor the $ErrorActionPreference setting, and 
# the error message still prints.
Foo

Desired behavior

No output.

Current behavior

Get-Item : Cannot find path '/Nosuch' because it does not exist.
...

Environment data

PowerShell Core v6.0.0-beta.5 on macOS 10.12.6
PowerShell Core v6.0.0-beta.5 on Ubuntu 16.04.3 LTS
PowerShell Core v6.0.0-beta.5 on Microsoft Windows 10 Pro (64-bit; v10.0.15063)
Windows PowerShell v5.1.15063.483 on Microsoft Windows 10 Pro (64-bit; v10.0.15063)

Activity

  1. added
    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassif
    on Aug 14, 2017
  2. iSazonov commented on Aug 14, 2017

    @iSazonov
    Collaborator

    Module has its own context. It may be preferable to set the preference variable value for the module
    Set-Variable -Module moduleName

  3. mklement0 commented on Aug 14, 2017

    @mklement0
    ContributorAuthor

    Yes, modules have their own scope, which is generally a good thing.

    However, this special case calls for a concise, PS-supported way to copy all the preference-variable values from the caller's scope.
    (The current workaround of using $PSCmdlet.SessionState.PSVariable based on a hard-coded list of preference variable names is neither future-proof nor reasonable.)

    Consider the user's perspective:

    If $ErrorActionPreference = 'Ignore'; Get-Item /NoSuch works,
    $ErrorActionPreference = 'Ignore'; Get-MyItem /NoSuch not working, just because the Get-MyItem command happens to be defined as an advanced function in a script module, is highly obscure and confusing.

    Giving module developers an easy, built-in way to opt into the caller's preferences would mitigate that problem.

  4. added this to the milestone on Aug 15, 2017
  5. iSazonov commented on Aug 15, 2017

    @iSazonov
    Collaborator

    Main module's function is to isolate code that expose public API and hide implementation. We can get unpredictable behavior if we just copy any variables (and maybe indirectly functions) to a module context.
    Also internally there are difference of global and local preference variables.
    Preference variables is not protected and can be used other ways:

    Remove-Variable ErrorActionPreference
    $ErrorActionPreference =@()

    We may have problems if these variables are simply copied in a module context.

    What scenarios do we need to? It seems it is only debug. If so we could resolve this otherwise, for example, by loading a module in debug mode. If we want to manage the preference variables in a module, we must do this in a predictable way (We can load tens modules in session!):

    Import-Module testModule -SetErrorActionPreference Stop
  6. iSazonov commented on Aug 15, 2017

    @iSazonov
    Collaborator

    Also we have an Issue with the request to share modules between runspaces. If we want implement this we can not simply copy any variables - no single parent context exists.

  7. mklement0 commented on Aug 15, 2017

    @mklement0
    ContributorAuthor

    Preference variables is not protected and can be used other ways:

    Indeed, but that is (a) incidental to the issue at hand and (b) should in itself be considered a bug: see #3483

  8. mklement0 commented on Aug 15, 2017

    @mklement0
    ContributorAuthor

    We can get unpredictable behavior if we just copy any variables (and maybe indirectly functions) to a module context.

    We're not talking about any variables. We're talking about the well-defined set of preference variables - even though, as stated, there's currently no programmatic way to enumerate them.

  9. mklement0 commented on Aug 15, 2017

    @mklement0
    ContributorAuthor

    What scenarios do we need to? It seems it is only debug.

    • Please read the blog post that the original post links to.
    • Then consider my Get-Item vs. Get-MyItem example.

    This is very much a production issue, unrelated to debugging:

    Currently, if you create a script module, that module's functions will by default ignore a caller's preferences variables (unless you're calling from the global scope), which to me is self-evidently problematic:

    For instance, if a caller sets $ErrorActionPreference to Stop, they have a reasonable expectation that cmdlets / advanced functions invoked subsequently honor that setting - without having to know whether a given command happens to be defined in a script module that doesn't see that setting by default.

  10. lzybkr commented on Aug 15, 2017

    @lzybkr
    Contributor

    Honoring the callers preference might cause serious problems by introducing exceptions (e.g. $errorActionPreference = 'Stop' in places where none were expected.

    Some examples - Dispose might not get called and resources leak, or the system may be left in an inconsistent state because some side effects do not execute.

    If the goal is to silence a noisy function - one can work with the function author to provide a better experience or else explicitly pass -ErrorActionPrefernce SilentlyContinue.

  11. dlwyatt commented on Aug 15, 2017

    @dlwyatt
    Contributor

    I think the focus is too much on the implementation here and not on the problem: a user shouldn't have to know or care how a particular PowerShell command is implemented. Whether it's a cmdlet, function, cdxml, imported from a remote session, whatever: they should have a consistent experience with regard to common parameters and preference variables. If I put $VerbosePreference = 'Continue' in my script, I should expect to see all the verbose output that I asked for. If I say $ErrorActionPreference = 'Stop', then anything that bubbles up through the Error stream to my code should cause a terminating error.

    Honoring the callers preference might cause serious problems by introducing exceptions (e.g. $errorActionPreference = 'Stop' in places where none were expected.

    I don't see this as a problem, since that's exactly what happens if the caller specifies -ErrorAction Stop today. (Common parameters cause the equivalent preference variables to be set in the advanced function's scope.)

  12. mklement0 commented on Aug 15, 2017

    @mklement0
    ContributorAuthor

    Dave Wyatt (@dlwyatt): Amen to that. I was in the middle of composing the following:

    The point is that if compiled cmdlets respect preference variables set in the caller's scope, it's reasonable to expect advanced functions to respect them too - the user shouldn't have to worry about module scopes or how a given command happens to be implemented.

    With respect to $ErrorActionPreference = 'Stop': It sounds like using Stop (whether via the pref. variable or the common parameter) is inherently problematic, which is a separate conversation (and I personally wasn't aware that it's problematic - probably worth documenting).

    Let's take a more innocuous example: setting $VerbosePreference = 'Continue' in an effort to make all cmdlet invocations in the current script produce verbose output:

    The following example defines Get-Foo1 as a compiled cmdlet, and functionally equivalent Get-Foo2 as an advanced function in a module:

    Get-Foo1 respects the verbose preference, Get-Foo2 does not.

    # Define compiled cmdlet Get-Foo1 (via an in-memory assembly and module).
    Add-Type -TypeDefinition @'
        using System;
        using System.Management.Automation;
        [Cmdlet("Get", "Foo1")]
        public class GetFoo1 : PSCmdlet {
    
            protected override void EndProcessing() {
                WriteVerbose("foo1");
            }
        }
    '@ -PassThru | % Assembly | Import-Module
    
    # Define analogous advanced function Get-Foo2 via an in-memory module.
    $null = New-Module {
        Function Get-Foo2 {
            [cmdletbinding()]
            param()
    
            End {
                Write-Verbose("foo2");
            }
        }
    }
    
    $VerbosePreference = 'Continue'
    
    # Compiled cmdlet respects $VerbosePreference.
    Get-Foo1
    # Verbose: foo1
    
    # Advanced function in script module does NOT, because it doesn't see the caller's
    # $VerbosePreference variable.
    Get-Foo2
    # (no output)

    Expecting the user to even anticipate a distinction here and to then know which commands are affected based on how they happen to be implemented strikes me as highly obscure.

    A scenario in which the behavior is even more obscure is with implicit remoting modules that proxy compiled cmdlets that execute remotely. In that case, the remotely executing cmdlets, even though they normally respect the caller's preference, do not. (On a related note: even -ErrorAction Stop does not work in that scenario, because the parameter is applied remotely, and terminating errors that occur remotely are by design converted to non-terminating ones.)


    I don't know what the right solution is, but if we can agree that there is a problem, we can tackle it.

  13. lzybkr commented on Aug 15, 2017

    @lzybkr
    Contributor

    Keep in mind one big difference between binary cmdlets and functions - binary cmdlets are rarely implemented in terms of PowerShell functions or other cmdlets.

    The error or verbose output is likely carefully crafted for a binary cmdlet, whereas it's a bit more random for functions.

    Also note that the equivalence of -ErrorAction Stop and setting the preference variable could be thought of as an implementation detail and is considered a bug by some people for some of the reasons mentioned above.

    It's worth pointing out that extra verbosity is not always desirable - and this proposal could turn some useful verbose output into noisy useless output.

  14. 61 remaining items

  15. JosephColvin commented on Apr 28, 2025

    @JosephColvin

    RiverHeart You could remove any variable ending with "preference" from being hardcoded by changing your variable definition of Preferences to:

    $Preferences = @(
                'ErrorView',                'FormatEnumerationLimit',           'Log*Event'
                'MaximumHistoryCount',      'OFS',                              'OutputEncoding',
                'PSDefaultParameterValues', 'PSEmailServer',                    'PSNativeCommandArgumentPassing'
                'PSSessionApplicationName', 'PSSessionConfigurationName',       'PSSessionOption'
                'PSStyle',                  'Transcript'
            ) + @(
                Get-ChildItem -Path "Variable:*preference" | Select-Object -ExpandProperty 'Name' 
            ) | 
            Where-Object {
                $IsIncluded = if ($Include) { $_ -in $Include } else { $True }
                $IsExcluded = if ($Exclude) { $_ -in $Exclude } else { $False }
                $IsIncluded -and -not $IsExcluded
            }

    If you would rather, you could also use Get-Variable -Scope 'Global' | Where-Object { $_.Name -ilike '*preference' } | Select-Object -ExpandProperty 'Name' to do the same thing instead of the Get-ChildItem -Path "Variable:*preference" | Select-Object -ExpandProperty 'Name' This of course does have the side effect of picking up any variable ending in the word "Preference" which I see why one wouldn't want to and why you would want to.

    Thank you though RiverHeart for sharing as that has proven to be far better than what I have been doing!

  16. boernsen-development commented on Jul 8, 2025

    @boernsen-development

    I tested the demonstration block from the first post and it seems to work as intended, i.e. no output is produced (as opposed to when I define $ErrorActionPreference = 'Stop'.

    Has this been fixed already?

    > $PSVersionTable 
    
    Name                           Value
    ----                           -----
    PSVersion                      7.5.2
    PSEdition                      Core
    GitCommitId                    7.5.2
    OS                             Microsoft Windows 10.0.22621
    Platform                       Win32NT
    PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
    PSRemotingProtocolVersion      2.3
    SerializationVersion           1.1.0.1
    WSManStackVersion              3.0
    
  17. RiverHeart commented on Jul 8, 2025

    @RiverHeart

    I tested the demonstration block from the first post and it seems to work as intended, i.e. no output is produced (as opposed to when I define $ErrorActionPreference = 'Stop'.

    Has this been fixed already?

    > $PSVersionTable 
    
    Name                           Value
    ----                           -----
    PSVersion                      7.5.2
    PSEdition                      Core
    GitCommitId                    7.5.2
    OS                             Microsoft Windows 10.0.22621
    Platform                       Win32NT
    PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
    PSRemotingProtocolVersion      2.3
    SerializationVersion           1.1.0.1
    WSManStackVersion              3.0
    

    I don't think so. If you run the example from the terminal it works since $ErrorActionPreference = 'Stop' also sets $Global:ErrorActionPreference which the module can see but if you put that code in a script and call it (eg ./CallPrefTest.ps1) instead of using ISE or VSCode it behaves as described since only script scope is changed.

  18. boernsen-development commented on Jul 17, 2025

    @boernsen-development

    Just another example showing that the -Confirm switch is ignored when calling other module's functions.

    Consider:

    $null = New-Module {
        function main {
            [CmdletBinding(SupportsShouldProcess)]
            param ()
    
            sub
        }
    }
    
    $null = New-Module {
        function sub {
            [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
            param ()
            
            if ($PSCmdlet.ShouldProcess("sub", "Write-Host")) {
                Write-Host "sub"
            }
        }
    }
    
    main -Confirm:$false

    Expected: no confirm question is asked due to -Confirm:$false
    Actual: the confirm question is asked despite -Confirm:$false

    This breaks automation scripts, which rely on -Confirm:$false. I hope this affirms the importance of this issue.

    Do you know of any simple workaround for this without much overhead?

  19. boernsen-development commented on Jul 17, 2025

    @boernsen-development

    Even worse with -WhatIf:

    $null = New-Module {
        function main {
            [CmdletBinding(SupportsShouldProcess)]
            param ()
            destructive
        }
    }
    
    $null = New-Module {
        function destructive {
            [CmdletBinding(SupportsShouldProcess)]
            param ()
            
            if ($PSCmdlet.ShouldProcess("sub", "destructive process")) {
                Write-Host "applying destructive process"
            }
        }
    }
    
    main -WhatIf

    Expected output: What if: Performing the operation "destructive process" on target "sub".
    Actual output: applying destructive process

  20. microsoft-github-policy-service commented on Jan 13, 2026

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if there is no further activity in 7 days, the issue will be closed automatically.

    Activity in this case refers only to comments on the issue. If the issue is closed and you are the author, you can re-open the issue using the button below. Please add more information to be considered during retriage. If you are not the author but the issue is impacting you after it has been closed, please submit a new issue with updated details and a link to this issue and the original.

  21. boernsen-development commented on Jan 13, 2026

    @boernsen-development

    Please don't close this. It's still important as it breaks automations, can cause unintended destructive operations and complicates debugging (see comments above).

  22. RiverHeart commented on Feb 13, 2026

    @RiverHeart

    Noticed another way to get the variables from the caller that is somewhat similar to Get-CallerPreference

    $null = New-Module {
      function Foo {
        [CmdletBinding()] param()
    
        @('ErrorActionPreference', 'VerbosePreference')) |
        Foreach-Object { $MyInvocation.MyCommand.Module.GetVariableFromCallersModule($_) } |
        Where-Object { $_.Value -ne $Null } |
        Foreach-Object { Set-Variable -Name $_.Name -Value $_.Value -Scope 'Script' -Verbose }
    
        # Provoke a non-terminating error.
        Get-Item /Nosuch
      }
    }
    
    $ErrorActionPreference = 'SilentlyContinue'
    
    Foo
  23. microsoft-github-policy-service commented on Nov 11, 2026

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if there is no further activity in 7 days, the issue will be closed automatically.

    Activity in this case refers only to comments on the issue. If the issue is closed and you are the author, you can re-open the issue using the button below. Please add more information to be considered during retriage. If you are not the author but the issue is impacting you after it has been closed, please submit a new issue with updated details and a link to this issue and the original.

  24. boernsen-development commented on Nov 12, 2026

    @boernsen-development

    Issue still exists. Workaround with custom solution (Get-CallerPreference) exists, but is error-prone as it requires importing and adding the call to every function. If it is forgotten and somebody relies on -WhatIf for destructive operations, it may lead to unexpected dataloss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassifIssue-Enhancementthe issue is more of a feature request than a bugWG-Enginecore PowerShell engine, interpreter, and runtime

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions