varvet/pundit · error · NotDefinedError
unable to find scope `#{find(object)}::Scope` for `#{object.
Error message
unable to find scope `#{find(object)}::Scope` for `#{object.inspect}` What it means
PolicyFinder#scope! resolves the policy class by convention (`#{model}Policy`) and then tries to constantize `#{policy}::Scope`. When that constant does not exist it raises NotDefinedError showing the exact name it searched for. It is the strict variant — `scope` returns nil in the same situation, so this error means the bang API was used and the lookup failed.
Source
Thrown at lib/pundit/policy_finder.rb:62
# @return [nil, Class] policy class with query methods
# @see https://github.com/varvet/pundit#policies
# @example
# policy = finder.policy #=> UserPolicy
# policy.show? #=> true
# policy.update? #=> false
#
# @since v0.1.0
def policy
klass = find(object)
klass.is_a?(String) ? klass.safe_constantize : klass
end
# @return [Scope{#resolve}] scope class which can resolve to a scope
# @raise [NotDefinedError] if scope could not be determined
#
# @since v0.1.0
def scope!
scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`"
end
# @return [Class] policy class with query methods
# @raise [NotDefinedError] if policy could not be determined
#
# @since v0.1.0
def policy!
policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
end
# @return [String] the name of the key this object would have in a params hash
#
# @since v1.1.0
def param_key # rubocop:disable Metrics/AbcSize
model = object.is_a?(Array) ? object.last : object
if model.respond_to?(:model_name)
model.model_name.param_key.to_sView on GitHub (pinned to 06318683c9)
Solutions
- Add a nested scope to the policy named in the message: `class Scope; def initialize(user, scope); ...; end; def resolve; ...; end; end`.
- If the message shows an unexpected constant name, fix the policy class name/namespace to match convention (model class name + Policy, mirroring the model's namespace).
- If you would rather get nil than a raise for uncovered models, call the non-bang `policy_scope` instead.
- When overriding lookup, make the model's `policy_class` (or the array namespace form) point at a class that actually contains a nested Scope.
Example fix
# before
class PostPolicy < ApplicationPolicy
def index?
false
end
# no Scope nested class
end
pundit.policy_scope!(Post) # NotDefinedError: unable to find scope `PostPolicy::Scope`
# after
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
scope.all
end
end
end Defensive patterns
Strategy: validation
Validate before calling
scope_class = Pundit::PolicyFinder.new(record).scope # non-bang: returns nil instead of raising if scope_class pundit.policy_scope!(record) # or policy_scope — class is known to exist now else # X::Scope missing: fall back, skip, or raise your own domain error end
Type guard
def policy_scope_defined?(record) !Pundit::PolicyFinder.new(record).scope.nil? end
Try / catch
begin
pundit.policy_scope!(record)
rescue Pundit::NotDefinedError => e
raise unless e.message.include?("::Scope") # distinguish scope-missing from policy-missing
# decide deliberately: unscoped access is usually NOT safe to default
raise MissingPolicyScopeError, e.message
end Prevention
- Treat every generated policy as incomplete until its nested Scope with #resolve exists; add a spec enumerating app/policies and asserting each policy defines Scope.
- Keep policy file paths mirroring model namespaces (Admin::Post -> app/policies/admin/post_policy.rb) so `"#{policy}::Scope"` constantizes.
- Use the bang variant only where absence is a bug; choose non-bang `policy_scope` plus an explicit nil check where absence is legitimate.
When it happens
Trigger: `pundit.policy_scope!(Post)`, `Pundit.policy_scope!(user, Post)`, or `Pundit::PolicyFinder.new(post).scope!` when: PostPolicy exists but has no nested `class Scope`; PostPolicy itself is missing (policy is nil, so `"#{policy}::Scope"` cannot resolve); a namespaced record `[:admin, Post]` lacks `Admin::PostPolicy::Scope`; a `policy_class` override on the model returns a class with no Scope nested inside.
Common situations: Generating a policy with query methods but omitting the nested Scope; namespaced models (Admin::Post) whose policy was scaffolded at top level; Rails/Zeitwerk naming mismatches where app/policies/admin/post_policy.rb defines the wrong constant; using symbols (`policy_scope!(:post)`) with no matching policy; switching from `policy_scope` (silent nil) to `policy_scope!` in a codebase with partially covered policies.
Related errors
- unable to find policy `#{find(object)}` for `#{object.inspec
- You must define #resolve in #{self.class}
- Invalid #<#{policy_scope_class}> constructor is called
- Invalid #<#{klass}> constructor is called
- 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/be6753698655b514.
Report an issue: GitHub.