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
- Inspect why UserObservationLink.create_or_restore returned nothing — check validation errors and uniqueness constraints on the link.
- Confirm the status value in the CSV is exactly 'active' or 'deleted' (trimmed, correct case); unknown values fall through without setting user_observer.
- Re-run the import after resolving conflicts from concurrent batches; avoid overlapping imports for the same user pair.
- 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
- Keep status values strictly 'active' or 'deleted' (lowercase, trimmed)
- Avoid running overlapping SIS batches for the same user pair
- Check UserObservationLink validations/constraints when failures cluster
- Re-verify any locally patched importer branches assign user_observer
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
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- A student referenced a non-existent user #
- An old_id, '# ', referenced a different # than the…
- An old_id, '# ', referenced a non-existent # and was not…
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)