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

  1. Use a distinct email address for the new user.
  2. Look up the existing user by email first; if present, update or re-send their invitation instead of creating.
  3. 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

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


AI-assisted analysis of strapi/strapi@4a4101264d (2026-08-12). Data as JSON: /api/errors/a72912a5f888c9fd. Report an issue: GitHub.