sahat/hackathon-starter · error · Error

EMAIL_COLLISION

EMAIL_COLLISION

Error message

EMAIL_COLLISION

What it means

Thrown when an OAuth signup for a brand-new provider identity would create an account whose normalized email already exists on another user. The app refuses to auto-merge accounts by email to prevent account takeover.

Source

Thrown at config/passport.js:129

    user.profile.location = user.profile.location || providerProfile.location;
    user.profile.website = user.profile.website || providerProfile.website;
    await user.save();
    return user;
  }
  // User is not logged in:
  const existingUser = await User.findOne({ [providerName]: { $eq: providerProfile.id } });
  if (existingUser) {
    return existingUser;
  }
  const normalizedEmail = providerProfile.email ? validator.normalizeEmail(providerProfile.email, { gmail_remove_dots: false }) : undefined;
  if (!normalizedEmail) {
    throw new Error('EMAIL_REQUIRED');
  }
  const existingEmailUser = await User.findOne({
    email: { $eq: normalizedEmail },
  });
  if (existingEmailUser) {
    throw new Error('EMAIL_COLLISION');
  }
  const user = new User();
  user.email = normalizedEmail;
  user[providerName] = providerProfile.id;
  req.user = user;
  if (oauth2provider) {
    await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, refreshTokenExpiration, providerName, tokenConfig);
  } else {
    user.tokens.push({ kind: providerName, accessToken, ...(tokenSecret && { tokenSecret }) });
  }
  user.profile.name = providerProfile.name;
  user.profile.gender = providerProfile.gender;

  if (providerProfile.picture) {
    user.profile.pictures = new Map();
    user.profile.pictures.set(providerName, providerProfile.picture);
    user.profile.picture = providerProfile.picture;
    user.profile.pictureSource = providerName;

View on GitHub (pinned to c12e339564)

Solutions

  1. Log in with the existing local account (email/password) and link the provider from the profile page instead
  2. Delete or rename the old account if it is a leftover test account
  3. As the operator, manually merge the provider id onto the existing user document

Example fix

// before: user hits EMAIL_COLLISION on new OAuth signup
// after (operator): attach provider id to the existing account
db.users.updateOne({ email: 'bob@example.com' }, { $set: { google: '12345' } });
Defensive patterns

Strategy: validation

Validate before calling

const emailUser = await User.findOne({ email: normalizedEmail });
if (emailUser && !emailUser[provider]) {
  // prompt: log in with existing account, then link provider
}

Try / catch

catch (e) { if (e.message === 'EMAIL_COLLISION') { req.flash('errors', {msg:'Email already registered. Log in with your password, then link this provider in your profile.'}); return res.redirect('/login'); } throw e; }

Prevention

When it happens

Trigger: User previously signed up locally with bob@example.com; now signs in with Google whose profile email normalizes to the same address; no user has google === profile.id yet, so existingEmailUser is found and the error is thrown.

Common situations: Signing in with a provider before realizing you already have a local account; case/dot variations of Gmail addresses normalizing to the same mailbox; testing OAuth with the same email across multiple providers.

Related errors


AI-assisted analysis of sahat/hackathon-starter@c12e339564 (2026-08-27). Data as JSON: /api/errors/0750d47ff721529b. Report an issue: GitHub.