toeverything/AFFiNE · error · EmailAlreadyUsed
email_already_used
email_already_used
Error message
This email has already been registered.
What it means
EmailAlreadyUsed, thrown by UserModel.create (user.ts:174-182): getUserByEmail(data.email, { withDisabled: true }) found an existing row. The lookup is case-insensitive (lower(email)) and includes disabled accounts, so 'A@x.com' collides with 'a@x.com' even when the existing account is disabled - duplicates are refused before the user row is inserted.
Solutions
- Sign in with (or reset the password of) the existing account instead of creating a new one
- If the existing account is disabled, re-enable it administratively rather than duplicating the email
- Normalize the email (trim + lowercase) before create, and map EmailAlreadyUsed to an 'already registered - sign in' UX
Example fix
// before
await user.create({ email, password }); // throws email_already_used
// after
const normalized = email.trim().toLowerCase();
try {
await user.create({ email: normalized, password });
} catch (e) {
if (e instanceof EmailAlreadyUsed) {
return res.status(409).json({ error: 'This email has already been registered.' });
}
throw e;
} Defensive patterns
Strategy: validation
Validate before calling
const normalized = data.email.trim().toLowerCase();
const existing = await user.getUserByEmail(normalized, { withDisabled: true });
if (existing) {
// prompt 'already registered - sign in or reset password' instead of create
} else {
await user.create({ ...data, email: normalized });
} Try / catch
try {
await user.create(data);
} catch (e) {
if (e instanceof EmailAlreadyUsed) {
return res.status(409).json({ error: 'This email has already been registered.' });
}
throw e;
} Prevention
- Normalize email casing (trim + lowercase) before create
- Remember the duplicate check includes disabled accounts and is case-insensitive
- Map the error to a sign-in/reset prompt, not a generic 500
When it happens
Trigger: create({ email, ... }) where any users row (enabled or disabled) already owns the same lowercased email.
Common situations: Re-registering after an account was disabled; OAuth signup colliding with a pre-existing local account; case-variant emails; retried signup requests.
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/e22d86a53f9fcb6b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/models/user.ts:132
return rows[0] ?? null;
}
async getPublicUserByEmail(email: string): Promise<PublicUser | null> {
const rows = await this.db.$queryRaw<PublicUser[]>`
SELECT id, name, avatar_url as "avatarUrl"
FROM "users"
WHERE lower("email") = lower(${email})
AND disabled = false
`;
return rows[0] ?? null;
}
async create(data: CreateUserInput) {
let user = await this.getUserByEmail(data.email, { withDisabled: true });
if (user) {
throw new EmailAlreadyUsed();
}
if (data.password) {
data.password = await this.crypto.encryptPassword(data.password);
}
user = await this.db.user.create({
data: {
...data,
name: data.name ?? data.email.split('@')[0],
},
});
// delegate the responsibility of finish user creating setup to the corresponding models
await this.event.emitAsync('user.postCreated', user);
this.logger.debug(`User [${user.id}] created with email [${user.email}]`);
this.event.emit('user.created', user);View on GitHub (pinned to 2af30773ae)