toeverything/AFFiNE · warning · SameEmailProvided

same_email_provided

same_email_provided

Error message

You are trying to update your account email to the same as the old one.

What it means

Thrown by the changeEmail GraphQL mutation when the requested new email already belongs to a user record whose id equals the authenticated user's id. Category is 'invalid_input', code 'same_email_provided'. It is intentionally distinct from EmailAlreadyUsed (which fires when a DIFFERENT account owns the email) so the client can show a precise, non-alarming message and keep the form open.

Source

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

    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,
      this.mailMetadata(context, expiresAt)
    );

View on GitHub (pinned to 26c515e050)

Solutions

  1. Compare the new email to the cached current user's email in the client and short-circuit (do not submit) when they are equal.
  2. Disable the submit/save button until the field value differs from the current email.
  3. Treat the error as informational on the client: show 'This is already your email' rather than a retryable error.

Example fix

// before: submit unconditionally
await changeEmail({ token, email: form.email, callbackUrl });

// after: guard against no-op
if (form.email.trim().toLowerCase() === currentUser.email.toLowerCase()) {
  setNotice('This is already your email.');
  return;
}
await changeEmail({ token, email: form.email, callbackUrl });
Defensive patterns

Strategy: validation

Validate before calling

function isSameEmail(newEmail: string, currentEmail: string): boolean {
  return newEmail.trim().toLowerCase() === currentEmail.trim().toLowerCase();
}
// before calling changeEmail:
if (isSameEmail(form.email, currentUser.email)) {
  setNotice('This is already your email.');
  return;
}

Type guard

function isEmailChangeValid(newEmail: string, currentEmail: string): newEmail is string {
  return !!newEmail && !isSameEmail(newEmail, currentEmail);
}

Prevention

When it happens

Trigger: Calling changeEmail (resolver.ts:272-279) with an `email` argument that, after getUserByEmail lookup, resolves to a row where hasRegistered.id === user.id. The token must already be valid (ChangeEmail token verified) before this check runs.

Common situations: A profile page that pre-fills the email field with the current address and the user submits without editing; stale form state after the email was already changed in another tab; a copy-paste of the existing address into the 'new email' box.

Related errors


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