RocketChat/Rocket.Chat · error · Error
error-room-is-already-on-hold
Error message
error-room-is-already-on-hold
What it means
Thrown by placeRoomOnHold when `room.onHold` is already true. The service treats on-hold as idempotent-fail: you cannot re-hold a chat that is currently held. The check sits immediately after the open-room check.
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 f9d3ec372b)
Solutions
- Make the UI idempotent: disable the on-hold control while the request is in flight and after room.onHold becomes true.
- Before calling, fetch the room and short-circuit if room.onHold is already true (treat as success rather than error).
- Dedupe concurrent hold requests by room id at the method/API layer.
Example fix
// before
await omnichannelService.placeRoomOnHold(room, comment, user);
// after
if (room.onHold) {
return { alreadyOnHold: true };
}
await omnichannelService.placeRoomOnHold(room, comment, user); Defensive patterns
Strategy: validation
Validate before calling
if (room.onHold) {
return { alreadyOnHold: true };
}
await omnichannelService.placeRoomOnHold(room, comment, user); Type guard
function isAlreadyOnHold(room: unknown): boolean {
return !!room && (room as any).onHold === true;
} Try / catch
try {
await omnichannelService.placeRoomOnHold(room, comment, user);
} catch (e) {
if (e instanceof Error && e.message === 'error-room-is-already-on-hold') return;
throw e;
} Prevention
- Treat onHold === true as a no-op success rather than an error.
- Disable the Hold button after the first successful hold request.
- Dedupe concurrent hold calls per room id.
When it happens
Trigger: Calling placeRoomOnHold on a room that is already on hold (onHold === true in LivechatRooms). Common via double-click on the hold button, a retry after a network blip, or two concurrent hold requests.
Common situations: User double-submits the hold action; frontend does not disable the button after the first success; a webhook and an agent both trigger hold on the same room.
Related errors
- error-room-not-on-hold
- error-room-already-closed
- error-unserved-rooms-cannot-be-placed-onhold
- This_conversation_is_already_closed
- This_conversation_is_already_closed
AI-assisted analysis of RocketChat/Rocket.Chat@f9d3ec372b (2026-08-12).
Data as JSON: /api/errors/eabada215cce0d95.
Report an issue: GitHub.