toeverything/AFFiNE · error · EmailAlreadyUsed

email_already_used

email_already_used

Error message

This email has already been registered.

What it means

In sendVerifyChangeEmail, after the ChangeEmail token verifies, the server checks whether the requested new email is already registered via models.user.getUserByEmail(email). If a user exists and its id differs from the current user.id, EmailAlreadyUsed is thrown — the new email belongs to someone else and cannot be claimed. (If it belongs to the same user, SameEmailProvided is thrown instead.) This prevents hijacking or merging onto an existing account.

Source

Thrown at packages/backend/server/src/core/auth/resolver.ts:276

    validators.assertValidEmail(email);
    const valid = await this.models.verificationToken.verify(
      TokenType.ChangeEmail,
      token,
      {
        credential: user.id,
      }
    );

    if (!valid) {
      throw new InvalidEmailToken();
    }

    const hasRegistered = await this.models.user.getUserByEmail(email);

    if (hasRegistered) {
      if (hasRegistered.id !== user.id) {
        throw new EmailAlreadyUsed();
      } else {
        throw new SameEmailProvided();
      }
    }

    const { token: verifyEmailToken, expiresAt } =
      await this.models.verificationToken.createWithExpiresAt(
        TokenType.VerifyEmail,
        user.id
      );

    const url = this.url.safeLink(callbackUrl, {
      token: verifyEmailToken,
      email,
    });
    return await this.auth.sendVerifyChangeEmail(
      email,
      url,

View on GitHub (pinned to 26c515e050)

Solutions

  1. Choose a new email address that is not registered to any other account.
  2. If the user owns the other account, sign into it instead, or have an admin delete/merge accounts out-of-band.
  3. Suggest the user verify the new email spelling before submitting.
  4. If the existing row is a stale duplicate, an admin can remove it to free the address.
Defensive patterns

Strategy: validation

Validate before calling

// client pre-check: warn if the new email looks taken (requires a lookup API)
// otherwise validate format and uniqueness intent before submit
if (newEmail === currentUser.email) {
  showUser('This is already your email.');
  return;
}

Type guard

function isEmailAlreadyUsed(err: unknown): boolean {
  return (
    !!err &&
    typeof err === 'object' &&
    (err as { code?: string }).code === 'email_already_used'
  );
}

Try / catch

try {
  await sendVerifyChangeEmail({ token, email: newEmail, callbackUrl });
} catch (err) {
  if (isEmailAlreadyUsed(err)) {
    showUser('That email is already registered to another account.');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: The user attempts to change their email to an address that is already the primary email of a DIFFERENT user account. The lookup returns a user whose id !== currentUser.id.

Common situations: User mistyped the new email as one already in use by another account. Two accounts with overlapping intended addresses. The new email was previously registered and abandoned but not deleted.

Related errors


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