instructure/canvas-lms · error · ImportError
A student referenced a non-existent user #
Error message
A student referenced a non-existent user #{student_id} What it means
During SIS user-observer import, process_user_observer looks up the student by sis_user_id among the root account's active pseudonyms. If no active pseudonym matches the given student_id, the importer cannot resolve the student and raises ImportError. This guards against creating observation links to users that do not exist (or are inactive/deleted) in Canvas.
Solutions
- Verify the student exists and is active: Account#pseudonyms.active.find_by(sis_user_id: student_id); fix or remove the row if not.
- Ensure the students users.csv (or users import) is processed in the same batch BEFORE the observers file, or in an earlier batch.
- Correct the student_id spelling / confirm it matches the sis_user_id on the student's pseudonym, not their login or canvas ID.
- Re-activate the student's pseudonym if it was suspended/deleted, then re-run the SIS import.
Example fix
# before (observers.csv references unknown student)
observer_id,student_id,status
user:obs1,user:studp99,active
# after (student must exist first in users.csv with matching sis_user_id)
users.csv: user_id,login_id,status
studp99,studp99,active
observers.csv: user:obs1,user:studp99,active Defensive patterns
Strategy: validation
Validate before calling
# before calling the SIS import, verify the student exists
student_id = 'studp99'
unless root_account.pseudonyms.active.exists?(sis_user_id: student_id)
raise "Skipping observer row: student #{student_id} not found/active"
end Try / catch
begin
importer.process_user_observer(obs_id, stud_id, status)
rescue SIS::ImportError => e
Rails.logger.warn("observer row skipped: #{e.message}")
# mark row as failed and continue the batch
end Prevention
- Import users.csv before user_observers.csv in every batch
- Validate observer/student SIS IDs against pseudonyms before generating import files
- Keep sis_user_ids stable; never rename them between exports
- Deactivate observer rows when the student is deleted
When it happens
Trigger: A user_observers.csv row whose student_id column contains an sis_user_id that is not an active pseudonym on the root account: the student was never imported, was deleted, their pseudonym is not 'active' (suspended), the sis_user_id was renamed, or the observer row was processed before the student row in the same SIS batch.
Common situations: Out-of-order/missing rows in an SIS zip (observer referenced before student import), typos in sis_user_id, students deleted or deactivated between exports, importing observers into a sub-account/root account where the student's pseudonym lives on a different shard or account, re-using IDs across environments (test vs prod).
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- Can't delete a non-existent observer for observer: #
- A cross-listing referenced a non-existent section #
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- An old_id, '# ', referenced a different # than the…
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/d02902bcfe65feb8.
Report an issue: GitHub.
Appendix: source
Thrown at lib/sis/user_observer_importer.rb:67
@logger = logger
@success_count = 0
@users_to_update_account_associations = Set.new
@user_observers_to_update_sis_batch_ids = []
end
def process_user_observer(observer_id, student_id, status)
raise ImportError, "No observer_id given for a user observer" if observer_id.blank?
raise ImportError, "No user_id given for a user observer" if student_id.blank?
raise ImportError, "Can't observe yourself user #{student_id}" if student_id == observer_id
raise ImportError, "Improper status \"#{status}\" for a user_observer" unless /\A(active|deleted)\z/i.match?(status)
return if @batch.skip_deletes? && status =~ /deleted/i
o_pseudo = @root_account.pseudonyms.active.find_by(sis_user_id: observer_id)
raise ImportError, "An observer referenced a non-existent user #{observer_id}" unless o_pseudo
s_pseudo = @root_account.pseudonyms.active.find_by(sis_user_id: student_id)
raise ImportError, "A student referenced a non-existent user #{student_id}" unless s_pseudo
observer = o_pseudo.user
student = s_pseudo.user
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
elseView on GitHub (pinned to 1c9f0bb801)