laurent22/joplin · error · ErrorUnprocessableEntity
there is already a user with this email
Error message
there is already a user with this email: ${user.email} What it means
UserModel.validate throws ErrorUnprocessableEntity('there is already a user with this email') when the email being saved is already owned by a different user. validate loads the user by email and only accepts the match if it is the same record (existingUser.id === user.id), so updating your own email to itself is allowed.
Solutions
- Choose a different email address for the account.
- If the account exists, update/reuse the existing user record instead of creating a new one.
- Check availability first via loadByEmail() before saving.
Example fix
// before
await models.user().save({ email: 'taken@example.com', password: 'x' }, { isNew: true });
// after
const existing = await models.user().loadByEmail('taken@example.com');
if (existing) throw new Error('Email already registered');
await models.user().save({ email: 'taken@example.com', password: 'x' }, { isNew: true }); Defensive patterns
Strategy: try-catch
Validate before calling
const existing = await models.user().loadByEmail(email); if (existing && existing.id !== userId) throw new Error('Email already in use'); Try / catch
try { await models.user().save(user, opts); } catch (e) { if (/already a user with this email/.test(e.message)) /* show 'email taken' UI */; } Prevention
- Check email availability before creating accounts; reuse the existing record when appropriate.
- Make import scripts idempotent by looking up users by email first.
When it happens
Trigger: Creating a new user with an email that already exists, or updating a user's email to one already taken by another account.
Common situations: Signup with an email that was previously registered; admin editing a user's email to a colleague's address; re-running an import script that creates duplicates.
Related errors
- email must be set
- Full name must be at most 256 characters
- password must be set
- Should include @ in email address, email
- Ambiguous notebook " ". Please use notebook id instead -…
AI-assisted analysis of laurent22/joplin@981a03c5c9 (2026-09-17).
Data as JSON: /api/errors/770fdcc281dff71f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/server/src/models/UserModel.ts:475
protected async validate(object: User, options: ValidateOptions = {}): Promise<User> {
const user: User = await super.validate(object, options);
// Note that we don't validate the password here because it's already
// been hashed by then.
if (options.isNew) {
if (!user.email) throw new ErrorUnprocessableEntity('email must be set');
if ('email' in user && !user.email.includes('@')) throw new ErrorUnprocessableEntity(`Should include @ in email address, email: ${user.email}`);
if (!user.password && !user.must_set_password) throw new ErrorUnprocessableEntity('password must be set');
} else {
if ('email' in user && !user.email) throw new ErrorUnprocessableEntity('email must be set');
if (user.email && !user.email.includes('@')) throw new ErrorUnprocessableEntity(`Should include @ in email address, email: ${user.email}`);
if ('password' in user && !user.password) throw new ErrorUnprocessableEntity('password must be set');
}
if (user.email) {
const existingUser = await this.loadByEmail(user.email);
if (existingUser && existingUser.id !== user.id) throw new ErrorUnprocessableEntity(`there is already a user with this email: ${user.email}`);
// See https://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690 (found via https://stackoverflow.com/a/574698)
if (user.email.length > 254) throw new ErrorUnprocessableEntity('Please enter an email address between 0 and 254 characters');
validateEmail(user.email);
}
if (user.full_name && user.full_name.length > 256) throw new ErrorUnprocessableEntity('Full name must be at most 256 characters');
return super.validate(user, options);
}
// public async delete(id: string): Promise<void> {
// const shares = await this.models().share().sharesByUser(id);
// await this.withTransaction(async () => {
// await this.models().item().deleteExclusivelyOwnedItems(id);
// await this.models().share().delete(shares.map(s => s.id));
// await this.models().userItem().deleteByUserId(id);
// await this.models().session().deleteByUserId(id);View on GitHub (pinned to 981a03c5c9)