RocketChat/Rocket.Chat · error · Meteor.Error

error-unit-not-found

error-unit-not-found

Error message

Unit not found

What it means

Thrown by saveUnit in LivechatEnterprise.ts:111 when editing an existing business unit (_id supplied) but the unit cannot be found within the caller's scoped set (unitsFromUser). It is a guard before reading ancestors for the update. Code is 'error-unit-not-found' (note the different prefix from 240) via Meteor.Error with method 'livechat:saveUnit'.

Source

Thrown at apps/meteor/ee/server/lib/omnichannel/LivechatEnterprise.ts:111

		check(unitDepartments, [
			Match.ObjectIncluding({
				departmentId: String,
			}),
		]);

		let ancestors: string[] = [];
		if (_id) {
			const unitsFromUser = await getUnitsFromUser(userId);
			const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(
				_id,
				{
					projection: { _id: 1, ancestors: 1 },
				},
				{ unitsFromUser },
			);
			if (!unit) {
				throw new Meteor.Error('error-unit-not-found', 'Unit not found', {
					method: 'livechat:saveUnit',
				});
			}

			ancestors = unit.ancestors || [];
		}

		const validUserMonitors = await Users.findUsersInRolesWithQuery(
			'livechat-monitor',
			{ _id: { $in: unitMonitors.map(({ monitorId }) => monitorId) } },
			{ projection: { _id: 1, username: 1 } },
		).toArray();

		const monitors = validUserMonitors.map(({ _id: monitorId, username }) => ({
			monitorId,
			username: username!,
		}));

View on GitHub (pinned to f9d3ec372b)

Solutions

  1. Re-fetch the unit list for the current userId and use a fresh _id.
  2. Confirm the user is assigned as a monitor of the target unit before allowing edit.
  3. Catch Meteor.Error 'error-unit-not-found' and surface a 'unit no longer available' message rather than retrying blindly.

Example fix

// before
await saveUnit(_id, unitData, monitors, depts, userId);

// after
const mine = await getUnitsFromUser(userId);
if (!_id || !mine.includes(_id)) {
  throw new Error('refresh unit list; _id not in user scope');
}
await saveUnit(_id, unitData, monitors, depts, userId);
Defensive patterns

Strategy: validation

Validate before calling

if (_id) {
  const mine = await getUnitsFromUser(userId);
  const unit = await LivechatUnit.findOneById(_id, { projection: { _id: 1 } }, { unitsFromUser: mine });
  if (!unit) throw new Error('refresh; unit not in scope');
}

Type guard

const canEditUnit = async (_id: string, userId: string): Promise<boolean> => {
  const mine = await getUnitsFromUser(userId);
  return Boolean(await LivechatUnit.findOneById(_id, { projection: { _id: 1 } }, { unitsFromUser: mine }));
};

Try / catch

try { await saveUnit(_id, unitData, monitors, depts, userId); }
catch (e) {
  if (e instanceof Meteor.Error && e.error === 'error-unit-not-found') { /* refresh UI */ return; }
  throw e;
}

Prevention

When it happens

Trigger: Calling livechat:saveUnit with a stale _id after the unit was deleted, or with a userId not assigned as a monitor of that unit. The scoped findOne (unitsFromUser) returns null even if the unit exists globally.

Common situations: Editing a business unit in a long-open form whose row was removed elsewhere; cross-tenant / cross-unit admin editing a unit they do not monitor; race between saveUnit and removeUnit.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@f9d3ec372b (2026-08-12). Data as JSON: /api/errors/25e317b901d4e30f. Report an issue: GitHub.