strapi/strapi · error · ApplicationError
Email already taken
Error message
Email already taken
What it means
Thrown by the user create controller when a user with the supplied email already exists. The controller picks the email from the parsed body, calls getService('user').exists({ email }), and on a truthy result throws ApplicationError('Email already taken') at user.ts:50. Admin users are uniquely keyed by email because it is the login identifier.
Source
Thrown at packages/core/admin/server/src/controllers/user.ts:44
const { body } = ctx.request as Create.Request;
const cleanData = { ...body, email: _.get(body, `email`, ``).toLowerCase() };
await validateUserCreationInput(cleanData);
const attributes = _.pick(cleanData, [
'firstname',
'lastname',
'email',
'roles',
'preferedLanguage',
]);
const userAlreadyExists = await getService('user').exists({
email: attributes.email,
});
if (userAlreadyExists) {
throw new ApplicationError('Email already taken');
}
const createdUser = await getService('user').create(attributes);
const userInfo = getService('user').sanitizeUser(createdUser);
// Note: We need to assign manually the registrationToken to the
// final user payload so that it's not removed in the sanitation process.
Object.assign(userInfo, { registrationToken: createdUser.registrationToken });
// Send 201 created
ctx.created({ data: userInfo } satisfies Create.Response);
},
async find(ctx: Context) {
const userService = getService('user');
const permissionsManager = strapi.service('admin::permission').createPermissionsManager({View on GitHub (pinned to 4a4101264d)
Solutions
- Use a distinct email address for the new user.
- Look up the existing user by email first; if present, update or re-send their invitation instead of creating.
- Ensure email normalization (lowercasing) is applied before the existence check so case-only differences are caught client-side.
Example fix
// before
await fetch('/admin/users', { method: 'POST', body: JSON.stringify({ email: 'admin@ex.com', ... }) });
// after
const [existing] = await strapi.query('admin::user').findMany({ where: { email: 'admin@ex.com'.toLowerCase() } });
if (existing) {
await strapi.service('admin::user').sendRegistration(existing);
} else {
await fetch('/admin/users', { method: 'POST', body: JSON.stringify({ email: 'admin@ex.com', ... }) });
} Defensive patterns
Strategy: validation
Validate before calling
const email = attributes.email.toLowerCase().trim();
const taken = await strapi.service('admin::user').exists({ email });
if (taken) throw new Error(`User email "${email}" already taken`); Type guard
const isUniqueUserEmail = async (email) => !(await strapi.service('admin::user').exists({ email: email.toLowerCase().trim() })); Try / catch
try {
await getService('user').create(attributes);
} catch (e) {
if (e instanceof ApplicationError && /Email already taken/.test(e.message)) {
// update existing user or re-send invitation
} else throw e;
} Prevention
- Make user-provisioning scripts idempotent by email.
- Normalize (lowercase, trim) emails before the existence check.
- Re-send invitations for existing users instead of recreating.
When it happens
Trigger: POST /admin/users with body.email matching an existing admin::user.email. Fires after input parse/normalize and before getService('user').create.
Common situations: Re-running a user-provisioning script that creates 'admin@ex.com' on every boot; inviting the same person twice; seeding a default admin in bootstrap after one already exists; case variants that normalize to the same address.
Related errors
- A user with this email address already exists
- Email already taken
- A user with this email address already exists
- Name already taken
- Name already taken
AI-assisted analysis of strapi/strapi@4a4101264d (2026-08-12).
Data as JSON: /api/errors/a72912a5f888c9fd.
Report an issue: GitHub.