toeverything/AFFiNE · error · EmailAlreadyUsed

email_already_used

email_already_used

Error message

This email has already been registered.

What it means

EmailAlreadyUsed, thrown by UserModel.create (user.ts:174-182): getUserByEmail(data.email, { withDisabled: true }) found an existing row. The lookup is case-insensitive (lower(email)) and includes disabled accounts, so 'A@x.com' collides with 'a@x.com' even when the existing account is disabled - duplicates are refused before the user row is inserted.

Solutions

  1. Sign in with (or reset the password of) the existing account instead of creating a new one
  2. If the existing account is disabled, re-enable it administratively rather than duplicating the email
  3. Normalize the email (trim + lowercase) before create, and map EmailAlreadyUsed to an 'already registered - sign in' UX

Example fix

// before
await user.create({ email, password }); // throws email_already_used

// after
const normalized = email.trim().toLowerCase();
try {
  await user.create({ email: normalized, password });
} catch (e) {
  if (e instanceof EmailAlreadyUsed) {
    return res.status(409).json({ error: 'This email has already been registered.' });
  }
  throw e;
}
Defensive patterns

Strategy: validation

Validate before calling

const normalized = data.email.trim().toLowerCase();
const existing = await user.getUserByEmail(normalized, { withDisabled: true });
if (existing) {
  // prompt 'already registered - sign in or reset password' instead of create
} else {
  await user.create({ ...data, email: normalized });
}

Try / catch

try {
  await user.create(data);
} catch (e) {
  if (e instanceof EmailAlreadyUsed) {
    return res.status(409).json({ error: 'This email has already been registered.' });
  }
  throw e;
}

Prevention

When it happens

Trigger: create({ email, ... }) where any users row (enabled or disabled) already owns the same lowercased email.

Common situations: Re-registering after an account was disabled; OAuth signup colliding with a pre-existing local account; case-variant emails; retried signup requests.

Related errors


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

Appendix: source

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

    return rows[0] ?? null;
  }

  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 2af30773ae)