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)
returnView on GitHub (pinned to ea665308ba)
Solutions
- Choose an email address not already registered in the system
- If the target account is yours and unused, delete/release the other account first, then retry
- Check for case/whitespace differences and normalize the input
- 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
- Normalize (trim/lowercase) emails before submitting
- Warn users when the entered email matches another of their accounts
- Disable double submission of the change-email form
- Keep a profile-level pre-check endpoint for email availability
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.