vxcontrol/pentagi · error

ErrChangeEmailCurrentUserEmailAlreadyExists

ErrChangeEmailCurrentUserEmailAlreadyExists

Error message

email already exists

What it means

ChangeEmailCurrentUser checks whether the requested new email is already taken before updating the user row; if a duplicate count is found it responds with ErrChangeEmailCurrentUserEmailAlreadyExists. Emails must be unique, and OAuth provider linking also keys off email, so the change is refused.

Source

Thrown at backend/pkg/server/services/users.go:247

		return
	}

	if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.CurrentPassword)); err != nil {
		logger.FromContext(c).WithError(err).Errorf("error checking password for current user")
		response.Error(c, response.ErrChangeEmailCurrentUserInvalidCurrentPassword, err)
		return
	}

	// Check if another user already has the new email
	var count int
	if err = s.db.Model(&models.User{}).Where("mail = ? AND id != ?", form.Mail, uid).Count(&count).Error; err != nil {
		logger.FromContext(c).WithError(err).Errorf("error checking email duplicate")
		response.Error(c, response.ErrInternal, err)
		return
	}
	if count > 0 {
		logger.FromContext(c).Errorf("email already exists: %s", form.Mail)
		response.Error(c, response.ErrChangeEmailCurrentUserEmailAlreadyExists, errors.New("email already exists"))
		return
	}

	// OAuth logins match accounts by email (authLoginCallback), so a new address unlinks the provider.
	updates := map[string]any{
		"mail":     form.Mail,
		"provider": nil,
	}

	if err = s.db.Model(&user).Scopes(scope).Updates(updates).Error; err != nil {
		if isUniqueViolation(err) {
			logger.FromContext(c).Warnf("email change rejected: address claimed concurrently")
			response.Error(c, response.ErrChangeEmailCurrentUserEmailAlreadyExists, errors.New("email already exists"))
			return
		}
		logger.FromContext(c).WithError(err).Errorf("error updating email for current user")
		response.Error(c, response.ErrInternal, err)
		return

View on GitHub (pinned to ea665308ba)

Solutions

  1. Choose an email address not already registered in the system
  2. If the target account is yours and unused, delete/release the other account first, then retry
  3. Check for case/whitespace differences and normalize the input
  4. If legitimately false-positive, verify the duplicate-count query scope (e.g. it should exclude the current user's own row)

Example fix

// before
{ "mail": "alice@example.com" }  // already used by another account
// after
{ "mail": "alice+work@example.com" }
Defensive patterns

Strategy: validation

Validate before calling

const email = form.mail.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new Error('invalid email');
const taken = await api.isEmailRegistered(email); // or rely on 409 handling
if (taken) throw new Error('email already exists');
await api.changeEmail({ mail: email });

Type guard

function isUsableNewEmail(v: unknown): v is string {
  return typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
}

Try / catch

try {
  await api.changeEmail({ mail });
} catch (e) {
  if (e.response?.data?.code === 'ErrChangeEmailCurrentUserEmailAlreadyExists') {
    setError('This email is already in use. Choose another address.');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: PATCH/PUT change-email for the current user with form.Mail equal to the mail of any existing user row — e.g. a user enters their other account's address or an address already registered via OAuth.

Common situations: Users with two accounts trying to consolidate, re-registering an old address, race where another signup took the address between form load and submit, or case-variation tricks that the uniqueness check still catches.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/63c755d443b3723d. Report an issue: GitHub.