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

  1. Implement #process_record(record) in the subclass with the per-record fixup logic
  2. If per-record processing is not needed, set mode to :range (or the appropriate mode) and implement the matching hook (e.g. #process)
  3. 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

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


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

    private

View on GitHub (pinned to 1c9f0bb801)