instructure/canvas-lms · error · NoMethodError
Subclasses must implement #process_record when mode is…
Error message
Subclasses must implement #process_record when mode is :individual_record
What it means
DataFixup is an abstract base class for shard-scoped data fixups. When mode is :individual_record, the engine calls #process_record per record; the base implementation is intentionally unimplemented and raises NoMethodError to force subclasses to define it. Hitting this means a fixup class declared :individual_record mode but never overrode process_record.
Solutions
- Implement #process_record(record) in the subclass with the per-record fixup logic
- If per-record processing is not needed, set mode to :range (or the appropriate mode) and implement the matching hook (e.g. #process)
- Add a spec asserting the fixup class responds to process_record before running it on production shards
Example fix
# before
class FixBrokenEnrollments < CanvasOperations::DataFixup
self.mode = :individual_record
end
# after
class FixBrokenEnrollments < CanvasOperations::DataFixup
self.mode = :individual_record
def process_record(enrollment)
enrollment.update!(workflow_state: 'completed')
end
end Defensive patterns
Strategy: validation
Validate before calling
raise ArgumentError, 'must implement #process_record' if fixup.mode == :individual_record && !(fixup.method(:process_record).owner != CanvasOperations::DataFixup)
Type guard
def individual_record_capable?(fixup_class)
fixup_class.mode != :individual_record ||
fixup_class.instance_method(:process_record).owner != CanvasOperations::DataFixup
end Try / catch
begin
fixup.process_range
rescue NoMethodError => e
raise e unless e.message.include?('process_record')
Rails.logger.error("#{fixup.class} missing #process_record for :individual_record mode")
end Prevention
- Always implement #process_record in the same commit that sets mode = :individual_record
- Add a shared spec for every DataFixup subclass asserting the mode/hook pairing
- Run new fixups against a test shard before production
When it happens
Trigger: A subclass of CanvasOperations::DataFixup sets mode to :individual_record (or inherits it) and process_range is invoked without the subclass defining its own #process_record.
Common situations: Creating a new data fixup and forgetting to implement process_record; copying a subclass that used :range or :batch mode but switching mode to :individual_record; a subclass silently renamed/removed the override so the base method is reached.
Understand the failure class
Background: "NotImplementedError: Subclasses should override this method" / "must be implemented" — abstract method errors explained — this error's family across 40 libraries.
Related errors
- Subclasses must implement #process_batch when mode is :batch
- No question type used when trying to parse a qti question
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- A student referenced a non-existent user #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/24691b6e4a8c4196.
Report an issue: GitHub.
Appendix: source
Thrown at lib/canvas_operations/data_fixup.rb:152
# Processes a batch of records. Ideal for making bulk updates to
# entire batches of rows that don't need to be individually loaded / processed.
#
# Subclasses must implement this method to define how to process a batch of records
#
# Only used when mode is set to :batch.
def process_batch(_batch)
raise NoMethodError, "Subclasses must implement #process_batch when mode is :batch"
end
# Processes an individual record. Ideal for when each record needs to be
# loaded and processed separately.
#
# Subclasses must implement this method to define how to process an individual record.
#
# Only used when mode is set to :individual_record.
def process_record(_record)
raise NoMethodError, "Subclasses must implement #process_record when mode is :individual_record"
end
# Determines if the current shard is a valid target for the DataFixup operation.
#
# In its base form this check returns true unless run_on_default_shard is false,
# in which case it returns true only if the current shard is not the default shard.
#
# Be wary if overriding this method. The default shard contains shadow copies of all
# root accounts, which may lead to unexpected fixup behavior.
#
# @return [Boolean] true if the current shard is not the default, false otherwise.
def valid_shard?
return true if self.class.run_on_default_shard?
!switchman_shard.default?
end
privateView on GitHub (pinned to 1c9f0bb801)