instructure/canvas-lms · error · ImportError

Failed to return user observer for observer: #

Error message

Failed to return user observer for observer: #{observer_id}, student: #{student_id}

What it means

After the status case block in add_remove_observer, the importer asserts that a user_observer record was produced — either created/restored for status 'active' or found-and-destroyed for 'deleted'. If the variable is nil/false the operation silently failed, so it raises ImportError. This is a post-condition check that the create_or_restore call (or delete path) actually yielded a usable link.

Solutions

  1. Inspect why UserObservationLink.create_or_restore returned nothing — check validation errors and uniqueness constraints on the link.
  2. Confirm the status value in the CSV is exactly 'active' or 'deleted' (trimmed, correct case); unknown values fall through without setting user_observer.
  3. Re-run the import after resolving conflicts from concurrent batches; avoid overlapping imports for the same user pair.
  4. If locally customized, ensure every case branch assigns user_observer or raises its own clearer error.

Example fix

# before (custom branch forgets to assign)
when "suspended"
  observer.as_observer_observation_links.for_root_accounts(@root_account).first&.suspend
# after
def add_remove_observer(observer, student, observer_id, student_id, status)
  case status.downcase
  when "suspended"
    user_observer = observer.as_observer_observation_links
                         .for_root_accounts(@root_account).first&.tap(&:suspend)
    raise ImportError, "No link to suspend" unless user_observer
Defensive patterns

Strategy: try-catch

Validate before calling

# pre-check the link can be created
next unless observer && student && observer != student
next unless observer.account == student.account # same root account

Try / catch

begin
  importer.process_user_observer(obs_id, stud_id, status)
rescue SIS::ImportError => e
  raise unless e.message.start_with?('Failed to return user observer')
  Rails.logger.error("create_or_restore failed for #{obs_id}/#{stud_id}: #{e.message}")
  raise # this indicates a real failure, usually worth surfacing
end

Prevention

When it happens

Trigger: UserObservationLink.create_or_restore returned nil for an 'active' row (e.g. creation failed validation, observer/student are in an invalid state, restore found nothing to restore), or a custom/patched status branch left user_observer unset (any status other than active/deleted that falls through the case without raising).

Common situations: Status values with unexpected casing or whitespace slipping past earlier regex checks, database constraint or validation failures swallowed inside create_or_restore, concurrent SIS batches racing on the same observer/student pair, locally patched importer code adding a status branch that forgets to assign user_observer.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/b43de9249e0aafdf. Report an issue: GitHub.

Appendix: source

Thrown at lib/sis/user_observer_importer.rb:89

        raise ImportError, "Can't observe yourself user #{student_id}" if observer == student

        add_remove_observer(observer, student, observer_id, student_id, status)
      end

      def add_remove_observer(observer, student, observer_id, student_id, status)
        case status.downcase
        when "active"
          check_observer_notification_settings(observer)
          user_observer = UserObservationLink.create_or_restore(observer:, student:, root_account: @root_account)
        when "deleted"
          user_observer = observer.as_observer_observation_links.for_root_accounts(@root_account).find_by(user_id: student)
          if user_observer
            user_observer.destroy
          else
            raise ImportError, "Can't delete a non-existent observer for observer: #{observer_id}, student: #{student_id}"
          end
        end
        raise ImportError, "Failed to return user observer for observer: #{observer_id}, student: #{student_id}" unless user_observer

        @users_to_update_account_associations.add observer.id
        @user_observers_to_update_sis_batch_ids << user_observer.id
        @success_count += 1
      end

      def check_observer_notification_settings(observer)
        if @root_account.settings[:default_notifications_disabled_for_observers]
          observer.default_notifications_disabled = true
          observer.save if observer.changed?
        end
      end
    end
  end
end

View on GitHub (pinned to 1c9f0bb801)