instructure/canvas-lms · error · CalendarEvent::ReservationError
cannot cancel past reservation
Error message
cannot cancel past reservation
What it means
CalendarEvent.reserve_for raises ReservationError when options[:cancel_existing] is set and one of the participant's existing reservations ends in the past. The code intentionally refuses to destroy reservations whose end_at is before Time.now.utc, since canceling a completed appointment would falsify history.
Solutions
- Remove options[:cancel_existing], or first delete the past reservation via the reservations API (past reservations can be deleted directly) before calling reserve_for.
- Only pass cancel_existing: true when the participant's existing reservations are all in the future.
- Use the appointment group's update_participants or a manual destroy of the stale reservation in the same transaction.
Example fix
// before
calendar_event.reserve_for(user, participant, cancel_existing: true)
// after
if participant.reservations.any? { |r| r.end_at < Time.now.utc }
participant.reservations.where('end_at < ?', Time.now.utc).each(&:destroy)
end
calendar_event.reserve_for(user, participant, cancel_existing: true) Defensive patterns
Strategy: validation
Validate before calling
past = appointment_group.reservations_for(participant).any? { |r| r.end_at < Time.now.utc }
raise UserFacingError, 'past reservation must be removed separately' if past && options[:cancel_existing] Type guard
def cancellable?(reservation) = reservation.end_at >= Time.now.utc
Try / catch
begin slot.reserve_for(user, participant, cancel_existing: true) rescue ReservationError => e handle_past_reservation_conflict(e) end
Prevention
- Filter out past reservations before requesting cancel_existing
- Delete expired reservations in a background cleanup job
- Never blind-pass cancel_existing when rebooking after an appointment date
When it happens
Trigger: Calling reserve_for on an appointment slot with options[:cancel_existing] => true while the participant holds a reservation whose end_at is earlier than the current UTC time.
Common situations: Rebooking a participant via the scheduler API after their old appointment already occurred; bulk-reschedule scripts that cancel existing reservations without filtering out past ones; UI flows letting users 'switch' into a new slot when their previous slot expired.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- participant has met per-participant limit
- all slots filled
- ineligible participant
- invalid participant
- not an appointment
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/3482bfc3330041d5.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/calendar_event.rb:601
end
def account
(context_type == "Account") ? context : nil
end
class ReservationError < StandardError; end
def reserve_for(participant, user, options = {})
raise ReservationError, "not an appointment" unless context_type == "AppointmentGroup"
raise ReservationError, "ineligible participant" unless context.eligible_participant?(participant)
transaction do
lock! # in case two people two participants try to grab the same slot
participant.lock! # in case two people try to make a reservation for the same participant
if options[:cancel_existing]
context.reservations_for(participant).lock.each do |reservation|
raise ReservationError, "cannot cancel past reservation" if reservation.end_at < Time.now.utc
reservation.updating_user = user
reservation.destroy
end
end
raise ReservationError, "participant has met per-participant limit" if context.max_appointments_per_participant && context.reservations_for(participant).size >= context.max_appointments_per_participant
raise ReservationError, "all slots filled" if participants_per_appointment && child_events.size >= participants_per_appointment
raise ReservationError, "participant has already reserved this appointment" if child_events_for(participant).present?
event = child_events.build
event.updating_user = user
event.context = participant
event.workflow_state = :locked
event.comments = options[:comments]
event.save!
if active?
self.workflow_state = "locked"View on GitHub (pinned to 1c9f0bb801)