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

  1. Provide a valid email address when creating the user.
  2. On update requests, omit the email key entirely if you do not intend to change it.
  3. 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

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


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)