varvet/pundit · error · InvalidConstructorError

Invalid #<#{klass}> constructor is called

Error message

Invalid #<#{klass}> constructor is called

What it means

Pundit::Context#cached_find backs `policy` and `policy!`: it resolves the policy class for a record, then builds it with `klass.new(user, model)`. If the policy class's constructor raises ArgumentError, pundit re-raises InvalidConstructorError naming that policy class. Policies (and policy caches) must follow the two-argument `(user, record)` initializer contract that ApplicationPolicy establishes.

Source

Thrown at lib/pundit/context.rb:168

    # @api private
    # @param record [Object] the object we're retrieving the policy for
    # @yield a policy finder if no policy was cached
    # @yieldparam [PolicyFinder] policy_finder
    # @yieldreturn [#new(user, model)]
    # @return [Policy, nil] an instantiated policy
    # @raise [InvalidConstructorError] if policy can't be instantated
    # @since v2.3.2
    def cached_find(record)
      policy_cache.fetch(user: user, record: record) do
        klass = yield policy_finder(record)
        next unless klass

        model = pundit_model(record)

        begin
          klass.new(user, model)
        rescue ArgumentError
          raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
        end
      end
    end

    # Return a policy finder for the given record.
    #
    # @api private
    # @return [PolicyFinder]
    # @since v2.3.2
    def policy_finder(record)
      PolicyFinder.new(record)
    end

    # Given a possibly namespaced record, return the actual record.
    #
    # @api private
    # @since v2.3.2
    def pundit_model(record)

View on GitHub (pinned to 06318683c9)

Solutions

  1. Inherit from ApplicationPolicy so the policy gets its `initialize(user, record)` for free.
  2. If the policy cannot inherit it, define `def initialize(user, record)` yourself; give any extra parameters defaults.
  3. Pass auxiliary objects through the user (e.g. `user.context`) instead of extra constructor arguments.
  4. Instantiate the class manually (`Pundit::PolicyFinder.new(post).policy.new(user, post)`) to see the underlying ArgumentError, since the re-raise drops the original message.

Example fix

# before
class PostPolicy # does not inherit ApplicationPolicy
  def initialize(user, record, audit_log) # pundit calls new(user, record): ArgumentError
    @user = user
    @record = record
  end
end

# after
class PostPolicy < ApplicationPolicy
  # ApplicationPolicy#initialize(user, record) is used as-is
  def show?
    user.admin? || record.user == user
  end
end
Defensive patterns

Strategy: validation

Validate before calling

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

policy = pundit.policy(record) if pundit_policy_constructible?(record)

Type guard

def two_arg_policy?(klass)
  return false if klass.nil?
  arity = klass.instance_method(:initialize).arity
  arity == 2 || arity == -1 || arity <= -3
end

Try / catch

begin
  pundit.policy(record)
rescue Pundit::InvalidConstructorError => e
  # klass is named in the message; see the real failure via:
  # Pundit::PolicyFinder.new(record).policy.new(pundit.user, record)
  raise
end

Prevention

When it happens

Trigger: `pundit.policy(post)`, `pundit.policy!(post)`, or `pundit.authorize(post, query: :show?, policy_class: nil)` (which routes through policy!) where the policy class defines `initialize(user)` with one argument, requires three (`(user, record, logger)`), or uses keyword-only params. The rescue at context.rb:167 converts the ArgumentError. Note: with an explicit `policy_class:` kwarg, authorize calls `policy_class.new` unguarded and raises plain ArgumentError instead.

Common situations: Hand-written PORO policies that skipped inheriting ApplicationPolicy and defined a different initializer; custom base policies adding auditing/context parameters; keyword-only initializers after a Ruby 3 upgrade; refactoring initialize signatures without updating the policy cache assumptions (the error also fires inside `policy_cache.fetch`, which can mislead the stack trace).

Related errors


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