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
- Change the Scope initializer to accept exactly `(user, scope)`, giving any extra parameters defaults: `def initialize(user, scope, context = nil)`.
- 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.
- When inheriting a custom base, keep `super(user, scope)` so the two-argument contract holds through the chain.
- 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
- Keep every Scope#initialize at exactly `(user, scope)`; give extra parameters defaults instead of required args.
- Never use keyword-only initializers in policy or scope classes — pundit always calls `new(user, record)` positionally.
- Add a spec that instantiates each policy and its Scope with two arguments; it catches signature drift the moment refactoring introduces it.
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
- Invalid #<#{klass}> constructor is called
- You must define #resolve in #{self.class}
- unable to find scope `#{find(object)}::Scope` for `#{object.
- unable to find policy `#{find(object)}` for `#{object.inspec
- No permissions in example metadata, did you forget to wrap w
AI-assisted analysis of varvet/pundit@06318683c9 (2026-08-21).
Data as JSON: /api/errors/286debabab2485a3.
Report an issue: GitHub.