calcom/cal.diy · error · UnprocessableEntityException

Can't reserve a slot if the event is already booked.

Error message

Can't reserve a slot if the event is already booked.

What it means

A NestJS UnprocessableEntityException (HTTP 422) from SlotsService_2024_09_04.reserveSlot. For a non-seated, non-round-robin event type, an active overlapping booking already exists. Since non-RR events host a single booking per slot, a second reservation is refused. Round-robin events skip this branch.

Source

Thrown at apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts:155

    );

    if (eventType.seatsPerTimeSlot) {
      const attendeesCount = booking?.attendees?.length;
      if (attendeesCount) {
        const seatsLeft = eventType.seatsPerTimeSlot - attendeesCount;
        if (seatsLeft < 1) {
          throw new UnprocessableEntityException(
            `Booking with id=${input.eventTypeId} at ${input.slotStart} has no more seats left.`
          );
        }
      }
    }

    const nonSeatedEventAlreadyBooked = !eventType.seatsPerTimeSlot && booking;
    const isRoundRobinEvent = eventType.schedulingType === SchedulingType.ROUND_ROBIN;

    if (nonSeatedEventAlreadyBooked && !isRoundRobinEvent) {
      throw new UnprocessableEntityException(`Can't reserve a slot if the event is already booked.`);
    }

    if (isRoundRobinEvent) {
      try {
        await validateRoundRobinSlotAvailability(input.eventTypeId, startDate, endDate, eventType.hosts);
      } catch (error) {
        if (error instanceof Error) {
          throw new UnprocessableEntityException(error?.message);
        }
        throw error;
      }
    } else {
      await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
    }

    const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;

    if (eventType.userId) {

View on GitHub (pinned to 176037d0af)

Solutions

  1. Re-fetch available slots and pick a free time before reserving.
  2. Treat 422 as a stale-availability signal and refresh the picker.
  3. For high-concurrency scenarios, consider a seated or round-robin event type.
  4. Implement optimistic retry: on 422, re-query once and offer the next slot.

Example fix

// before — retry the same slot after a failure
fetch('/v2/slots/reserve', { body: JSON.stringify({ eventTypeId, slotStart }) });

// after — refresh and pick a free slot on 422
try { await reserve(eventTypeId, slotStart); }
catch (e) {
  if (e.status === 422) {
    const free = await nextFreeSlot(eventTypeId);
    await reserve(eventTypeId, free.start);
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function ensureSlotFree(eventTypeId: number, slotStart: string) {
  const slots = await cal.slots.list({ eventTypeId, start: slotStart, end: oneDayLater(slotStart) });
  const hit = Object.values(slots).flat().find(s => s.start === slotStart && !s.away);
  if (!hit) throw new UserFacingError('That time was just booked — choose another.');
  return hit;
}
await ensureSlotFree(eventTypeId, slotStart);

Type guard

function isFreeSlot(slot: unknown): slot is { start: string; away?: boolean } {
  return typeof slot === 'object' && slot !== null
    && typeof (slot as any).start === 'string'
    && !(slot as any).away;
}

Try / catch

try {
  await cal.slots.reserve({ eventTypeId, slotStart });
} catch (e) {
  if (e instanceof HttpError && e.statusCode === 422 && /already booked/i.test(e.message)) {
    const next = await nextFreeSlot(eventTypeId);
    return cal.slots.reserve({ eventTypeId, slotStart: next.start });
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /v2/slots/reserve for a standard (non-seated, non-round-robin) event type where the host already has an active booking overlapping the requested start.

Common situations: A booking was created between the time the slot list was fetched and the reserve call; the client retries a reserve after the booking completed; concurrent reservation attempts for the same slot.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/d725a786e35405c3. Report an issue: GitHub.