puppetlabs/puppet · error · Puppet::Error

Attempt to assign variable %{name} when evaluating parameter

Error message

Attempt to assign variable %{name} when evaluating parameters

What it means

Raised by ParameterScope#[]= while a parameter default expression is being evaluated. evaluate()/evaluate3x() wrap default evaluation in as_read_only, which sets @read_only = true; any write into a parameter during that window (variable assignment landing on the ParameterScope) is rejected. Its purpose is to keep default expressions side-effect free, since assigning to parameters mid-evaluation would corrupt the ordered evaluation contract.

Source

Thrown at lib/puppet/parser/scope.rb:234

    private :parameter_reference_failure

    def initialize(parent, callee_name, param_names)
      super(parent)
      @callee_name = callee_name
      @params = {}
      param_names.each { |name| @params[name] = Access.new }
    end

    def [](name)
      access = @params[name]
      return super if access.nil?

      throw(:unevaluated_parameter, name) unless access.assigned?
      access.value
    end

    def []=(name, value)
      raise Puppet::Error, _("Attempt to assign variable %{name} when evaluating parameters") % { name: name } if @read_only

      @params[name] ||= Access.new
      @params[name].value = value
    end

    def bound?(name)
      @params.include?(name)
    end

    def include?(name)
      @params.include?(name) || super
    end

    def is_local_scope?
      true
    end

    def as_read_only

View on GitHub (pinned to e227c27540)

Solutions

  1. Make the helper side-effect free: have the function RETURN a value used as the default instead of assigning a scope variable: `$a = my_func()`.
  2. Move the assignment out of the default expression into the class body: `class c($a = undef) { $a = my_func() if $a == undef }`.
  3. If you maintain the Ruby function, replace scope.setvar with a return value, or write to a named (non-parameter) scope explicitly.

Example fix

# before (3.x function writes scope)
module Puppet::Parser::Functions
  newfunction(:set_port) do |args|
    self.scope.setvar('port', args[0])   # raises when called from a default
  end
end
class app($port = set_port(8080)) { }

# after
module Puppet::Parser::Functions
  newfunction(:get_port) do |args|
    args[0]
  end
end
class app($port = get_port(8080)) { }
Defensive patterns

Strategy: validation

Validate before calling

# Audit Ruby 3.x functions for scope mutation before reusing them in defaults:
#   rg "setvar|scope\[.*\]\s*=" lib/ 
# A default expression may only CALL functions that return values.

Prevention

When it happens

Trigger: A parameter default whose evaluation assigns a scope variable that resolves into the ParameterScope — e.g. a legacy 3.x Ruby function called from a default expression that does scope.setvar / scope['x'] = ..., or an expression whose block/lambda shares the guarded scope and performs `$x = ...`. Example: `class c($a = set_scope_var('x'))` where the 3.x function writes scope['x']. API users hit it by calling ParameterScope#[]= directly while @read_only is true.

Common situations: Old 3.x-style modules whose Ruby functions mutate the scope being reused under Puppet 4+ parameter evaluation; wrapping default expressions with functions that cache results in scope variables; upgrading Puppet versions where evaluation order/read-only enforcement became stricter.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/153ce0bc8d720d65. Report an issue: GitHub.