RocketChat/Rocket.Chat · error · Error
error-room-not-on-hold
Error message
error-room-not-on-hold
What it means
Thrown by resumeRoomOnHold (omnichannel.internalService.ts:94) when the room passed to the resume flow does not have onHold truthy. The method only resumes conversations that are currently on hold, so a room that was never held or was already resumed fails this precondition before any database change.
Solutions
- Re-fetch the room right before resuming and verify room.onHold === true
- Make the resume action idempotent in the UI: once a resume succeeds, remove the on-hold state from local room and disable the action
- Guard server-side callers with an explicit onHold check so only genuinely held rooms are resumed
Example fix
// before
await omnichannelService.resumeRoomOnHold(room, comment, agent);
// after
if (!room.onHold) {
// nothing to resume — treat as success or notify the user
return;
}
await omnichannelService.resumeRoomOnHold(room, comment, agent); Defensive patterns
Strategy: validation
Validate before calling
const room = await LivechatRooms.findOneById(rid, { projection: { t: 1, open: 1, onHold: 1, servedBy: 1 } });
if (!room?.onHold) {
// nothing to resume — chat is not on hold
return;
}
await omnichannelService.resumeRoomOnHold(room, comment, agent); Type guard
const isRoomOnHold = (room: IOmnichannelRoom | null | undefined): room is IOmnichannelRoom & { onHold: true } =>
!!room && isOmnichannelRoom(room) && room.onHold === true; Try / catch
try {
await omnichannelService.resumeRoomOnHold(room, comment, agent);
} catch (e) {
if (e instanceof Error && e.message === 'error-room-not-on-hold') {
// treat as already-resumed: refresh room state, no retry
}
throw e;
} Prevention
- Make the resume action idempotent: after a successful resume clear the local onHold flag and disable the button
- Pre-check room.onHold on freshly fetched data before invoking the service
- Debounce double-clicks on the resume control
When it happens
Trigger: Calling resumeRoomOnHold with a room whose onHold is false or undefined — e.g. double-resume (second click after the first succeeded), a room never placed on hold, or a stale room object fetched before another flow resumed it.
Common situations: Duplicate 'resume' submissions from the client; stale room state after the on-hold flag was cleared by a concurrent resume; automation/server code resuming rooms that are simply open, not on hold.
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
- error-invalid-inquiry
- error-invalid-room
- error-room-already-closed
- error-room-is-already-on-hold
- error-room-not-served
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/105cb716d6d4f2e4.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/ee/server/local-services/omnichannel.internalService.ts:94
async resumeRoomOnHold(
room: Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'servedBy'>,
comment: string,
resumeBy: Pick<IUser, '_id' | 'username' | 'name'>,
clientAction = false,
) {
this.logger.debug({ msg: 'Attempting to resume room on hold', roomId: room._id, userId: resumeBy?._id });
if (!room || !isOmnichannelRoom(room)) {
throw new Error('error-invalid-room');
}
if (!room.open) {
throw new Error('This_conversation_is_already_closed');
}
if (!room.onHold) {
throw new Error('error-room-not-on-hold');
}
const { _id: roomId, servedBy } = room;
if (!servedBy) {
this.logger.error({ msg: 'No serving agent found for room', roomId });
throw new Error('error-room-not-served');
}
const inquiry = await LivechatInquiry.findOneByRoomId(roomId, {});
if (!inquiry) {
this.logger.error({ msg: 'No inquiry found for room', roomId });
throw new Error('error-invalid-inquiry');
}
await this.attemptToAssignRoomToServingAgentElseQueueIt({
room,
inquiry,View on GitHub (pinned to b2c16d5842)