RocketChat/Rocket.Chat · error · Error
error-room-already-closed
Error message
error-room-already-closed
What it means
placeRoomOnHold only operates on open conversations: after the room-type guard, if room.open is false it throws Error('error-room-already-closed'). Holding a chat exists to pause a live conversation for later resume, which is meaningless once the room is closed.
Solutions
- Re-fetch the room and check open before invoking; disable the on-hold action for closed rooms in the UI.
- Catch 'error-room-already-closed' and treat it as benign - the desired end state (chat not open) already holds.
- Sequence automation so hold attempts cannot be enqueued after close requests.
Example fix
// before
await service.placeRoomOnHold(staleRoom, comment, user); // staleRoom.open was true in the client only
// after
const fresh = await LivechatRooms.findOneById(rid, { projection: { t: 1, open: 1, onHold: 1, servedBy: 1 } });
if (!fresh?.open) return; // already closed; nothing to hold
await service.placeRoomOnHold(fresh, comment, user); Defensive patterns
Strategy: validation
Validate before calling
const fresh = await LivechatRooms.findOneById(rid, { projection: { t: 1, open: 1, onHold: 1, servedBy: 1 } });
if (!fresh?.open) {
// already closed; holding is not applicable - skip the call
} Try / catch
try {
await service.placeRoomOnHold(room, comment, user);
} catch (e: any) {
if (e?.message === 'error-room-already-closed') return; // desired end state already holds; benign
throw e;
} Prevention
- Re-fetch room.open immediately before hold operations.
- Disable the hold action in the UI when the room is closed or on the close event.
- Do not enqueue hold jobs from stale room snapshots.
When it happens
Trigger: Calling placeRoomOnHold after the conversation was closed - the agent closed it, inactivity timeout closed it, or an API close raced the hold. Typical shape: stale client state letting the agent click 'on hold' on a chat that is already closed.
Common situations: Outdated room data in the client after a close event; two agents concurrently closing and holding the same chat; automation acting on an old room snapshot.
Related errors
- error-room-is-already-on-hold
- error-unserved-rooms-cannot-be-placed-onhold
- error-cannot-place-chat-on-hold
- error-invalid-room
- error-room-onHold
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/d86cd311d99c5cf5.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/ee/server/local-services/omnichannel.internalService.ts:44
constructor() {
super();
this.logger = new Logger('OmnichannelEE');
}
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),View on GitHub (pinned to b2c16d5842)