laurent22/joplin · error · ErrorUnprocessableEntity

Should include @ in email address, email

Error message

Should include @ in email address, email: ${user.email}

What it means

UserModel.validate throws ErrorUnprocessableEntity('Should include @ in email address') when the provided email string does not contain an '@'. This is a cheap sanity check applied to both new users and updates before the more thorough validateEmail() RFC check runs later.

Solutions

  1. Send a properly formatted email address containing '@'.
  2. Run client-side syntax validation (must contain '@') before submitting.
  3. Trim whitespace — a stray space is still caught, so sanitize input first.

Example fix

// before
await models.user().save({ email: 'johndoe', password: 'x' }, { isNew: true });
// after
const email = 'john@example.com';
if (!email.includes('@')) throw new Error('Invalid email');
await models.user().save({ email, password: 'x' }, { isNew: true });
Defensive patterns

Strategy: validation

Validate before calling

if (email && !email.includes('@')) throw new Error('Invalid email format');

Type guard

function looksLikeEmail(s: string): boolean { return s.includes('@'); }

Try / catch

try { await models.user().save(user, opts); } catch (e) { if (/Should include @ in email/.test(e.message)) /* show format error */; }

Prevention

When it happens

Trigger: Saving or updating a user whose email value is a non-address string lacking '@', e.g. 'johndoe' or 'foo.bar'.

Common situations: Import scripts mapping the wrong CSV column into email; users typing a username instead of an email; API clients sending the display name as email.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of laurent22/joplin@981a03c5c9 (2026-09-17). Data as JSON: /api/errors/8a839b89f302580d. Report an issue: GitHub.

Appendix: source

Thrown at packages/server/src/models/UserModel.ts:469

			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> {
	// 	const shares = await this.models().share().sharesByUser(id);

View on GitHub (pinned to 981a03c5c9)