RocketChat/Rocket.Chat · warning · Error
error-room-closed
error-room-closed
Error message
error-room-closed
What it means
The inner doCloseRoom throws Error('error-room-closed') when the room object is missing, is not an omnichannel room (isOmnichannelRoom fails), or is not open while forceClose is unset. Unlike the outer 'error-room-already-closed', it also rejects rooms of the wrong type reaching the omnichannel close pipeline (e.g. a regular channel).
Source
Thrown at apps/meteor/server/lib/omnichannel/closeRoom.ts:136
void notifyOnRoomChangedById(newRoom._id);
if (inquiry) {
void notifyOnLivechatInquiryChanged(inquiry, 'removed');
}
logger.debug({ msg: 'Room was closed', roomId: newRoom._id });
}
async function doCloseRoom(
params: CloseRoomParams,
session: ClientSession,
): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> {
const { comment } = params;
const { room, forceClose } = params;
logger.debug({ msg: 'Attempting to close room', roomId: room._id, forceClose });
if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) {
logger.debug({ msg: 'Room is not open', roomId: room._id });
throw new Error('error-room-closed');
}
const commentRequired = settings.get('Livechat_request_comment_when_closing_conversation');
if (commentRequired && !comment?.trim()) {
throw new Error('error-comment-is-required');
}
const { updatedOptions: options } = await resolveChatTags(room, params.options);
logger.debug({ msg: 'Resolved chat tags for room', roomId: room._id });
const now = new Date();
const { _id: rid, servedBy } = room;
const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000;
const closeData: IOmnichannelRoomClosingInfo = {
closedAt: now,
chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000,
...(serviceTimeDuration && { serviceTimeDuration }),View on GitHub (pinned to b2c16d5842)
Solutions
- Verify isOmnichannelRoom(room) && room.open before invoking closeRoom
- Pass forceClose: true when intentionally re-closing an already-closed omnichannel room
- Use the regular channel close APIs for non-omnichannel rooms instead of this pipeline
Defensive patterns
Strategy: type-guard
Validate before calling
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) {
// skip closeRoom: wrong room type or already closed
} Type guard
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
const isClosableLivechatRoom = (room: any, forceClose = false): room is IOmnichannelRoom =>
!!room && isOmnichannelRoom(room) && (forceClose || room.open === true); Try / catch
try {
await closeRoom({ room, user, comment });
} catch (err: any) {
if (err?.message === 'error-room-closed') return alreadyClosed(room._id);
throw err;
} Prevention
- Filter rooms through isOmnichannelRoom before feeding any omnichannel close pipeline
- Keep regular-channel close logic separate from livechat close logic
- Re-check room.open immediately before the close call in racy UIs
When it happens
Trigger: Passing a non-omnichannel room (team channel, DM) into closeRoom; closing an omnichannel room that was already closed without forceClose; a race where the room got closed between being fetched and doCloseRoom running.
Common situations: Integrations that look rooms up by name and accidentally feed a non-livechat room into the close flow; shared close logic reused for regular channels; double-close races.
Related errors
- error-room-already-closed
- error-room-does-not-exist
- error-invalid-room
- invalid-room
- message-length-exceeds-character-limit
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/db6ac8dc4dee07cb.
Report an issue: GitHub.