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

  1. Verify isOmnichannelRoom(room) && room.open before invoking closeRoom
  2. Pass forceClose: true when intentionally re-closing an already-closed omnichannel room
  3. 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

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


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