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
- Read the target's subscription roles first and only offer 'Remove moderator' when the array includes 'moderator'
- Refresh room members before rendering role actions
- 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
- Only show 'Remove moderator' when the subscription roles include moderator
- Refresh roles before actions
- Treat role-absence errors as no-ops in idempotent flows
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
- error-user-not-owner
- error-invalid-user
- error-not-allowed
- error-user-not-in-room
- error-user-already-leader
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/da70f4d61fa09140.
Report an issue: GitHub.