{"record":{"id":"eabada215cce0d95","repo":"RocketChat/Rocket.Chat","slug":"error-room-is-already-on-hold","errorCode":null,"errorMessage":"error-room-is-already-on-hold","messagePattern":"error-room-is-already-on-hold","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/ee/server/local-services/omnichannel.internalService.ts","lineNumber":47,"sourceCode":"\t}\n\n\tasync placeRoomOnHold(\n\t\troom: Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold'>,\n\t\tcomment: string,\n\t\tonHoldBy: Pick<IUser, '_id' | 'username' | 'name'>,\n\t) {\n\t\tthis.logger.debug({ msg: 'Attempting to place room on hold', roomId: room._id, userId: onHoldBy?._id });\n\n\t\tconst { _id: roomId } = room;\n\n\t\tif (!room || !isOmnichannelRoom(room)) {\n\t\t\tthrow new Error('error-invalid-room');\n\t\t}\n\t\tif (!room.open) {\n\t\t\tthrow new Error('error-room-already-closed');\n\t\t}\n\t\tif (room.onHold) {\n\t\t\tthrow new Error('error-room-is-already-on-hold');\n\t\t}\n\t\tconst restrictedOnHold = settings.get('Livechat_allow_manual_on_hold_upon_agent_engagement_only');\n\t\tconst canRoomBePlacedOnHold = !room.onHold;\n\t\tconst canAgentPlaceOnHold = !room.lastMessage?.token;\n\t\tconst canPlaceChatOnHold = canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold);\n\t\tif (!canPlaceChatOnHold) {\n\t\t\tthrow new Error('error-cannot-place-chat-on-hold');\n\t\t}\n\t\tif (!room.servedBy) {\n\t\t\tthrow new Error('error-unserved-rooms-cannot-be-placed-onhold');\n\t\t}\n\n\t\tconst [roomResult, subsResult] = await Promise.all([\n\t\t\tLivechatRooms.setOnHoldByRoomId(roomId),\n\t\t\tSubscriptions.setOnHoldByRoomId(roomId),\n\t\t\tMessage.saveSystemMessage<IOmnichannelSystemMessage>('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }),\n\t\t]);\n","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/f9d3ec372bb580fa8d036f94cf03925a478ef768/apps/meteor/ee/server/local-services/omnichannel.internalService.ts#L29-L65","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait omnichannelService.placeRoomOnHold(room, comment, user);\n\n// after\nif (room.onHold) {\n  return { alreadyOnHold: true };\n}\nawait omnichannelService.placeRoomOnHold(room, comment, user);","handlingStrategy":"validation","validationCode":"if (room.onHold) {\n  return { alreadyOnHold: true };\n}\nawait omnichannelService.placeRoomOnHold(room, comment, user);","typeGuard":"function isAlreadyOnHold(room: unknown): boolean {\n  return !!room && (room as any).onHold === true;\n}","tryCatchPattern":"try {\n  await omnichannelService.placeRoomOnHold(room, comment, user);\n} catch (e) {\n  if (e instanceof Error && e.message === 'error-room-is-already-on-hold') return;\n  throw e;\n}","preventionTips":["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."],"tags":["omnichannel","livechat","room-state","idempotency"],"backgroundTag":null,"analyzedSha":"f9d3ec372bb580fa8d036f94cf03925a478ef768","analyzedAt":"2026-08-12T19:07:17.372Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}