toeverything/AFFiNE · error · EmailAlreadyUsed
email_already_used
email_already_used
Error message
This email has already been registered.
What it means
Thrown by UserModel.create (user.ts:180) when getUserByEmail(withDisabled:true) finds any user at that email — disabled accounts included — to prevent duplicate identities and shadow accounts. The lookup is case-insensitive on lower(email). UserFriendlyError, code email_already_used, type resource_already_exists, HTTP 400.
Source
Thrown at packages/backend/server/src/models/user.ts:180
return user;
}
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 26c515e050)
Solutions
- Sign in instead of signing up.
- If the existing account is disabled, have an admin re-enable it rather than recreating it.
- Normalize (trim + lowercase) emails before lookup to avoid case/whitespace collisions.
- For imports, skip or update existing rows instead of calling create.
Defensive patterns
Strategy: validation
Validate before calling
async function createIfFree(models, input: CreateUserInput) {
const existing = await models.user.getUserByEmail(input.email, { withDisabled: true });
if (existing) {
throw new Error('Email already registered; sign in or re-enable the account.');
}
return models.user.create(input);
} Try / catch
import { EmailAlreadyUsed } from '../base/error/errors.gen';
try {
await models.user.create(input);
} catch (e) {
if (e instanceof EmailAlreadyUsed) {
// redirect to sign-in / password reset
} else throw e;
} Prevention
- Normalize email (trim + lowercase) before lookup to avoid case/whitespace duplicates.
- Treat disabled accounts as occupied when checking availability.
- For imports, skip-or-update existing rows rather than calling create blindly.
When it happens
Trigger: POST /sign-up or UserModel.create with an email that already exists (active or disabled); re-importing a user CSV that contains already-existing emails.
Common situations: User forgot they already registered; a disabled account still occupies the email; test fixtures collide with real users; case variants (User@x vs user@x) collide because the lookup is case-insensitive.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/e22d86a53f9fcb6b.
Report an issue: GitHub.