toeverything/AFFiNE · error · EmailAlreadyUsed

email_already_used

email_already_used

Error message

This email has already been registered.

What it means

Thrown by UserModel.create (user.ts:180) when getUserByEmail(withDisabled:true) finds any user at that email — disabled accounts included — to prevent duplicate identities and shadow accounts. The lookup is case-insensitive on lower(email). UserFriendlyError, code email_already_used, type resource_already_exists, HTTP 400.

Source

Thrown at packages/backend/server/src/models/user.ts:180

    return user;
  }

  async getPublicUserByEmail(email: string): Promise<PublicUser | null> {
    const rows = await this.db.$queryRaw<PublicUser[]>`
      SELECT id, name, avatar_url as "avatarUrl"
      FROM "users"
      WHERE lower("email") = lower(${email})
      AND disabled = false
    `;

    return rows[0] ?? null;
  }

  async create(data: CreateUserInput) {
    let user = await this.getUserByEmail(data.email, { withDisabled: true });

    if (user) {
      throw new EmailAlreadyUsed();
    }

    if (data.password) {
      data.password = await this.crypto.encryptPassword(data.password);
    }

    user = await this.db.user.create({
      data: {
        ...data,
        name: data.name ?? data.email.split('@')[0],
      },
    });

    // delegate the responsibility of finish user creating setup to the corresponding models
    await this.event.emitAsync('user.postCreated', user);

    this.logger.debug(`User [${user.id}] created with email [${user.email}]`);
    this.event.emit('user.created', user);

View on GitHub (pinned to 26c515e050)

Solutions

  1. Sign in instead of signing up.
  2. If the existing account is disabled, have an admin re-enable it rather than recreating it.
  3. Normalize (trim + lowercase) emails before lookup to avoid case/whitespace collisions.
  4. For imports, skip or update existing rows instead of calling create.
Defensive patterns

Strategy: validation

Validate before calling

async function createIfFree(models, input: CreateUserInput) {
  const existing = await models.user.getUserByEmail(input.email, { withDisabled: true });
  if (existing) {
    throw new Error('Email already registered; sign in or re-enable the account.');
  }
  return models.user.create(input);
}

Try / catch

import { EmailAlreadyUsed } from '../base/error/errors.gen';

try {
  await models.user.create(input);
} catch (e) {
  if (e instanceof EmailAlreadyUsed) {
    // redirect to sign-in / password reset
  } else throw e;
}

Prevention

When it happens

Trigger: POST /sign-up or UserModel.create with an email that already exists (active or disabled); re-importing a user CSV that contains already-existing emails.

Common situations: User forgot they already registered; a disabled account still occupies the email; test fixtures collide with real users; case variants (User@x vs user@x) collide because the lookup is case-insensitive.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/e22d86a53f9fcb6b. Report an issue: GitHub.