RocketChat/Rocket.Chat · error · Error

error-room-is-already-on-hold

Error message

error-room-is-already-on-hold

What it means

If room.onHold is already true, placeRoomOnHold throws Error('error-room-is-already-on-hold') rather than treating the repeat call as a no-op. The check runs third, after the room-type and open guards, so it fires on genuinely open livechat rooms that are already held.

Solutions

  1. Check room.onHold before calling and treat an already-held room as success.
  2. Prevent duplicate submissions: disable the button after click, add idempotency keys for retried requests.
  3. Re-fetch the room immediately before the call so the decision uses fresh state.

Example fix

// before
await service.placeRoomOnHold(room, comment, user); // second click -> error-room-is-already-on-hold

// after
if (room.onHold) return { alreadyOnHold: true }; // idempotent: already in the desired state
await service.placeRoomOnHold(room, comment, user);
Defensive patterns

Strategy: validation

Validate before calling

if (room.onHold) {
	// already held; treat as success and skip the call (idempotency at the caller)
}

Try / catch

try {
	await service.placeRoomOnHold(room, comment, user);
} catch (e: any) {
	if (e?.message === 'error-room-is-already-on-hold') return { alreadyOnHold: true }; // idempotent success
	throw e;
}

Prevention

When it happens

Trigger: Double-invoking the on-hold action (double click, retried HTTP request without idempotency), or holding a room that another agent or automation already placed on hold.

Common situations: Duplicate submissions from flaky clients or retries; UI not reflecting the onHold flag set moments earlier by a different agent/session; bulk scripts processing a room list that overlaps with a concurrent hold job.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/eabada215cce0d95. Report an issue: GitHub.

Appendix: source

Thrown at apps/meteor/ee/server/local-services/omnichannel.internalService.ts:47

	}

	async placeRoomOnHold(
		room: Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold'>,
		comment: string,
		onHoldBy: Pick<IUser, '_id' | 'username' | 'name'>,
	) {
		this.logger.debug({ msg: 'Attempting to place room on hold', roomId: room._id, userId: onHoldBy?._id });

		const { _id: roomId } = room;

		if (!room || !isOmnichannelRoom(room)) {
			throw new Error('error-invalid-room');
		}
		if (!room.open) {
			throw new Error('error-room-already-closed');
		}
		if (room.onHold) {
			throw new Error('error-room-is-already-on-hold');
		}
		const restrictedOnHold = settings.get('Livechat_allow_manual_on_hold_upon_agent_engagement_only');
		const canRoomBePlacedOnHold = !room.onHold;
		const canAgentPlaceOnHold = !room.lastMessage?.token;
		const canPlaceChatOnHold = canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold);
		if (!canPlaceChatOnHold) {
			throw new Error('error-cannot-place-chat-on-hold');
		}
		if (!room.servedBy) {
			throw new Error('error-unserved-rooms-cannot-be-placed-onhold');
		}

		const [roomResult, subsResult] = await Promise.all([
			LivechatRooms.setOnHoldByRoomId(roomId),
			Subscriptions.setOnHoldByRoomId(roomId),
			Message.saveSystemMessage<IOmnichannelSystemMessage>('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }),
		]);

View on GitHub (pinned to b2c16d5842)