laurent22/joplin · error · ErrorUnprocessableEntity
email must be set
Error message
email must be set
What it means
UserModel.validate throws ErrorUnprocessableEntity('email must be set') when a user record is saved without an email address. For new users (isNew) an email is always required; for updates it is required only if the 'email' key is present in the object being saved (i.e. you explicitly tried to set a falsy email).
Solutions
- Provide a valid email address when creating the user.
- On update requests, omit the email key entirely if you do not intend to change it.
- Validate form input before calling save() to ensure email is non-empty.
Example fix
// before
await models.user().save({ password: 'x', must_set_password: true }, { isNew: true });
// after
await models.user().save({ email: 'user@example.com', password: 'x', must_set_password: true }, { isNew: true }); Defensive patterns
Strategy: validation
Validate before calling
if (!email) throw new Error('email is required'); Type guard
function hasEmail(u: { email?: string }): u is { email: string } & typeof u { return typeof u.email === 'string' && u.email.length > 0; } Try / catch
try { await models.user().save(user, opts); } catch (e) { if (e.message === 'email must be set') /* surface field-level error to form */; } Prevention
- Mark email as required in signup/creation forms and validate before submit.
- On PATCH requests, only include the email key when actually changing it.
When it happens
Trigger: Saving a new user (isNew: true) with no email, or patching a user with { email: '' } / { email: null }.
Common situations: Signup or admin-create flows where the form did not include the email field; bulk import code omitting the email column; a PATCH request explicitly blanking the email.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- password must be set
- Full name must be at most 256 characters
- Should include @ in email address, email
- there is already a user with this email
- Ambiguous notebook " ". Please use notebook id instead -…
AI-assisted analysis of laurent22/joplin@981a03c5c9 (2026-09-17).
Data as JSON: /api/errors/e1351caccfe124e0.
Report an issue: GitHub.
Appendix: source
Thrown at packages/server/src/models/UserModel.ts:468
let msg: string[] = [result.feedback.warning];
if (result.feedback.suggestions) {
msg = msg.concat(result.feedback.suggestions);
}
throw new ErrorUnprocessableEntity(msg.join(' '));
}
}
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> {View on GitHub (pinned to 981a03c5c9)