RocketChat/Rocket.Chat · error · Meteor.Error

error-user-not-moderator

error-user-not-moderator

Error message

User is not a moderator

What it means

removeRoomModerator only proceeds when the target's subscription carries the 'moderator' role. If subscription.roles is truthy but either not an array or does not include 'moderator', it throws error-user-not-moderator. Quirk: a null/undefined roles field slips past this check.

Source

Thrown at apps/meteor/server/meteor-methods/rooms/removeRoomModerator.ts:57

	const user = await Users.findOneById(userId);

	if (!user?.username) {
		throw new Meteor.Error('error-invalid-user', 'Invalid user', {
			method: 'removeRoomModerator',
		});
	}

	const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id);

	if (!subscription) {
		throw new Meteor.Error('error-invalid-room', 'Invalid room', {
			method: 'removeRoomModerator',
		});
	}

	if (subscription.roles && (!Array.isArray(subscription.roles) || !subscription.roles.includes('moderator'))) {
		throw new Meteor.Error('error-user-not-moderator', 'User is not a moderator', {
			method: 'removeRoomModerator',
		});
	}

	await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'user' });

	const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'moderator');
	await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.filter((r) => r !== 'moderator') || []);

	if (removeRoleResponse.modifiedCount) {
		void notifyOnSubscriptionChangedById(subscription._id);
	}

	const fromUser = await Users.findOneById(fromUserId);
	if (!fromUser) {
		throw new Meteor.Error('error-invalid-user', 'Invalid user', {
			method: 'removeRoomModerator',
		});

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Read the target's subscription roles first and only offer 'Remove moderator' when the array includes 'moderator'
  2. Refresh room members before rendering role actions
  3. Catch error-user-not-moderator and treat it as 'already in the desired state'

Example fix

// before
await Meteor.callAsync('removeRoomModerator', rid, userId);

// after
const sub = Subscriptions.findOne({ rid, 'u._id': userId });
const roles = sub?.roles;
if (!Array.isArray(roles) || !roles.includes('moderator')) return;
await Meteor.callAsync('removeRoomModerator', rid, userId);
Defensive patterns

Strategy: validation

Validate before calling

const sub = Subscriptions.findOne({ rid, 'u._id': userId });
if (!Array.isArray(sub?.roles) || !sub.roles.includes('moderator')) {
  // not a moderator: skip or mark as already demoted
}

Type guard

const hasRoomRole = (roles: string[] | undefined | null, role: string): boolean =>
  Array.isArray(roles) && roles.includes(role);

const isModerator = (sub?: { roles?: string[] }): boolean => hasRoomRole(sub?.roles, 'moderator');

Try / catch

try {
  await Meteor.callAsync('removeRoomModerator', rid, userId);
} catch (err) {
  if (err instanceof Meteor.Error && err.error === 'error-user-not-moderator') {
    // already demoted: refresh and continue
  }
}

Prevention

When it happens

Trigger: Calling removeRoomModerator on a plain member or an owner-only user; the moderator role was already stripped by another admin; subscription.roles holds a corrupted non-array truthy value.

Common situations: Members list still showing a stale 'Moderator' badge after the role was removed elsewhere; double-click racing the UI into issuing the demotion twice.

Related errors


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