varvet/pundit · error · InvalidConstructorError

Invalid #<#{policy_scope_class}> constructor is called

Error message

Invalid #<#{policy_scope_class}> constructor is called

What it means

Pundit::Context#policy_scope looks up the record's `X::Scope` class and instantiates it with exactly two positional arguments, `new(user, pundit_model(scope))`. If that constructor call raises ArgumentError — almost always a mismatch between the number of arguments your Scope#initialize accepts and the two Pundit passes — it is re-raised as InvalidConstructorError naming the offending scope class.

Source

Thrown at lib/pundit/context.rb:116

    # @!endgroup

    # @!group Scopes

    # Retrieves the policy scope for the given record.
    #
    # @see https://github.com/varvet/pundit#scopes
    # @param scope [Object] the object we're retrieving the policy scope for
    # @raise [InvalidConstructorError] if the policy constructor called incorrectly
    # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
    # @since v2.3.2
    def policy_scope(scope)
      policy_scope_class = policy_finder(scope).scope
      return unless policy_scope_class

      begin
        policy_scope = policy_scope_class.new(user, pundit_model(scope))
      rescue ArgumentError
        raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
      end

      policy_scope.resolve
    end

    # Retrieves the policy scope for the given record. Raises if not found.
    #
    # @see https://github.com/varvet/pundit#scopes
    # @param scope [Object] the object we're retrieving the policy scope for
    # @raise [NotDefinedError] if the policy scope cannot be found
    # @raise [InvalidConstructorError] if the policy constructor called incorrectly
    # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
    # @since v2.3.2
    def policy_scope!(scope)
      policy_scope_class = policy_finder(scope).scope!

      begin
        policy_scope = policy_scope_class.new(user, pundit_model(scope))

View on GitHub (pinned to 06318683c9)

Solutions

  1. Change the Scope initializer to accept exactly `(user, scope)`, giving any extra parameters defaults: `def initialize(user, scope, context = nil)`.
  2. If extra data is required, pass it through the user object or a custom policy/scope class set via the model's `policy_class` override, instead of extra constructor args.
  3. When inheriting a custom base, keep `super(user, scope)` so the two-argument contract holds through the chain.
  4. Reproduce the underlying ArgumentError by calling `Pundit::PolicyFinder.new(Post).scope.new(user, Post)` directly — InvalidConstructorError hides the original message, so this shows the real arity problem.

Example fix

# before
class PostPolicy < ApplicationPolicy
  class Scope < ApplicationPolicy::Scope
    def initialize(user, scope, tenant) # Pundit calls new(user, scope): ArgumentError
      super(user, scope)
      @tenant = tenant
    end
  end
end

# after
class PostPolicy < ApplicationPolicy
  class Scope < ApplicationPolicy::Scope
    def initialize(user, scope, tenant = nil)
      super(user, scope)
      @tenant = tenant || user&.tenant
    end
  end
end
Defensive patterns

Strategy: validation

Validate before calling

def pundit_two_arg_constructor?(klass)
  return false if klass.nil?
  arity = klass.instance_method(:initialize).arity
  arity == 2 || arity == -1 || arity <= -3 # exact 2, splat, or 2 required + optionals
end

scope_class = Pundit::PolicyFinder.new(record).scope
result = pundit.policy_scope(record) if pundit_two_arg_constructor?(scope_class)

Type guard

def pundit_scope_safe?(record)
  scope_class = Pundit::PolicyFinder.new(record).scope
  return false if scope_class.nil?
  arity = scope_class.instance_method(:initialize).arity
  arity == 2 || arity == -1 || arity <= -3
end

Try / catch

begin
  pundit.policy_scope(record)
rescue Pundit::InvalidConstructorError => e
  # the original ArgumentError message is masked — reproduce it to see the real arity mismatch:
  # Pundit::PolicyFinder.new(record).scope.new(pundit.user, record)
  logger.error("#{e.message} — check #{Pundit::PolicyFinder.new(record).scope}#initialize arity")
  raise
end

Prevention

When it happens

Trigger: `pundit.policy_scope(Post)` (or `Pundit.policy_scope(user, Post)`) where `PostPolicy::Scope#initialize(user)` expects one argument, `initialize(user, scope, context)` requires three, or the initializer is keyword-only (`def initialize(user:, scope:)`, arity 0 positional). Any of these makes `.new(user, model)` raise ArgumentError, which the rescue at context.rb:115 converts.

Common situations: Custom base policy scopes that add extra constructor parameters (tenant, context, request); porting scopes from another authorization library with a different initializer signature; refactoring ApplicationPolicy#initialize and forgetting the nested Scope; keyword-args-only initializers introduced during a Ruby 3 upgrade.

Related errors


AI-assisted analysis of varvet/pundit@06318683c9 (2026-08-21). Data as JSON: /api/errors/286debabab2485a3. Report an issue: GitHub.