Skip to main content
Being Idea Innovations
Regex Explained, and Why Email Validation Is Not a Regex Problem
Back to Blog

Regex Explained, and Why Email Validation Is Not a Regex Problem

26 April 20236 min readJavascript
Share:

Email validation is the most common reason people reach for a regular expression, and it is also the thing regular expressions are worst at. Understanding why teaches you a lot about what regex is genuinely good for.

Regex fundamentals

A regular expression describes a pattern of characters. In JavaScript you write one between slashes:

const pattern = /cat/;
pattern.test('concatenate');   // true — "cat" appears somewhere

Anchors

/^cat/     // must START with "cat"
/cat$/     // must END with "cat"
/^cat$/    // must be EXACTLY "cat"

Forgetting anchors is the single most common regex bug. /\d{5}/ matches a five-digit postcode — and also matches those five digits buried inside a longer string. If you are validating rather than searching, anchor both ends.

Character classes

[abc]      // any one of a, b, c
[^abc]     // any character EXCEPT a, b, c
[a-z]      // any lowercase letter
[0-9]      // any digit — same as \d
\d \w \s   // digit, word character (letters/digits/underscore), whitespace
\D \W \S   // the negations of the above
.          // any character except a newline

Quantifiers

a?         // 0 or 1
a*         // 0 or more
a+         // 1 or more
a{3}       // exactly 3
a{2,4}     // between 2 and 4
a{2,}      // 2 or more

Quantifiers are greedy by default — they take as much as possible. Add ? to make one lazy:

'<a><b>'.match(/<.+>/)[0];    // "<a><b>"  — greedy, takes everything
'<a><b>'.match(/<.+?>/)[0];   // "<a>"     — lazy, stops at the first match

Groups and flags

(abc)              // capturing group — extract it later
(?:abc)            // non-capturing — group without capturing
(?<year>\d{4})     // named group

/cat/i   // case-insensitive
/cat/g   // global — find all matches
/cat/m   // multiline — ^ and $ match at line breaks
/cat/u   // unicode — required for \p{…} and correct emoji handling
const m = '2026-07-24'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year;   // "2026"

Email validation: the honest answer

Here is the pattern almost everyone uses, including the earlier version of this article:

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

Reading it left to right: start of string, one or more characters that are not whitespace or @, an @, more non-whitespace non-@ characters, a literal dot, more of the same, end of string.

This is a good pattern. Not because it is accurate — it is not — but because it catches the realistic mistakes (missing @, missing domain, a stray space from copy-paste) without rejecting anything a real user is likely to type.

What it gets wrong, in both directions

It accepts things that are not deliverable:

emailRegex.test('a@b.c');              // true — valid shape, almost certainly not real
emailRegex.test('user@example.invalid'); // true — reserved TLD, cannot exist
emailRegex.test('user@@example.com');    // false, good
emailRegex.test('user@example..com');    // true — double dot, not valid

And it rejects addresses that are genuinely valid under the specification:

emailRegex.test('user@localhost');       // false — valid on intranets
emailRegex.test('"john doe"@example.com'); // false — quoted local parts are legal
emailRegex.test('user@[192.168.1.1]');   // false — IP literals are legal

Those last three are rare enough that rejecting them is a defensible product decision. But be aware you are making a decision, not applying a rule.

Why you should not "just use the RFC-compliant regex"

RFC 5322 permits quoted strings, comments in parentheses, escaped characters and nested constructs. A regex that implements it faithfully runs to several thousand characters. Nobody on your team can read it, nobody can safely modify it, and it still does not tell you whether the mailbox exists.

The trade-off is not "loose regex versus strict regex". It is "a simple pattern that catches typos" versus "an unmaintainable pattern that catches typos".

The security issue: ReDoS

Some clever-looking email patterns contain nested quantifiers, and those can be exploited. Consider:

// Vulnerable — nested quantifier
const bad = /^([a-zA-Z0-9]+)*@example\.com$/;

bad.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!');   // hangs

The engine tries every possible way to split that run of as between the inner + and the outer *. The number of combinations doubles with each added character, so roughly thirty characters is enough to lock a CPU core.

This is Regular expression Denial of Service. On a Node server it is worse than a slow response — JavaScript is single-threaded, so one malicious input freezes the entire process for every user.

The warning signs are a quantifier applied to a group that itself contains a quantifier — (a+)*, (a|aa)+, (\s*\w+)*. Note that the simple email pattern above is safe: [^\s@]+ cannot overlap with @, so there is nothing to backtrack over.

Validate in layers

1. HTML — free, and it gives you the keyboard

<input type="email" name="email" required autocomplete="email" />

Browsers validate this natively and show a localised error. On mobile it also switches the on-screen keyboard to one with @ and . visible, which is worth more to your conversion rate than any regex.

Note that HTML5's type="email" deliberately uses a willful violation of RFC 5322 — a pragmatic subset. Even the specification authors concluded strict compliance was the wrong goal.

2. Client-side JavaScript — instant feedback

const form = document.querySelector('#signup');
const input = form.elements.email;
const error = document.querySelector('#email-error');

input.addEventListener('blur', () => {
  const value = input.value.trim();

  if (value === '') {
    setError('Email is required.');
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    setError('Enter a valid email address, such as name@example.com.');
  } else {
    setError('');
  }
});

function setError(message) {
  error.textContent = message;
  input.setAttribute('aria-invalid', message ? 'true' : 'false');
}

Validate on blur, not on every keystroke — telling someone their address is invalid while they are still typing the third character is irritating. And point aria-describedby at the error element so screen readers announce it.

3. Server-side — the only side that counts

Client validation is a convenience. Anyone can bypass it. In PHP:

$email = trim($_POST['email'] ?? '');

if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    $errors[] = 'Enter a valid email address.';
}

FILTER_VALIDATE_EMAIL implements a well-tested subset and is more reliable than anything you will write by hand. It is slightly stricter than the spec — it rejects user@localhost, for instance — which for a public signup form is usually what you want.

Optionally confirm the domain can actually receive mail:

[$local, $domain] = explode('@', $email, 2);

if (!checkdnsrr($domain, 'MX') && !checkdnsrr($domain, 'A')) {
    $errors[] = 'That email domain does not appear to accept mail.';
}

Use this carefully. It adds a DNS lookup to your signup path, and a temporary DNS failure will reject a perfectly good address.

4. A confirmation email — the actual validation

No amount of pattern matching tells you whether bob@example.com belongs to Bob, exists, or accepts mail. The only way to know is to send a message with a signed, expiring link and see whether someone clicks it.

Everything before this step is about catching typos so users are not left wondering why their confirmation never arrived. That is a real and worthwhile goal — just not the same as validation.

Normalise before storing

$email = strtolower(trim($email));

Domains are case-insensitive. Local parts are technically case-sensitive, but virtually every provider treats them as insensitive, so lowercasing prevents duplicate accounts for Bob@example.com and bob@example.com.

Resist the urge to strip Gmail's dots or +tags. Those are legitimate and users rely on them for filtering; removing them breaks their setup and surprises them.

Summary

  • Anchor validation patterns with ^ and $.
  • Use /^[^\s@]+@[^\s@]+\.[^\s@]+$/ for email — simple, readable, ReDoS-safe.
  • Do not chase RFC compliance with regex; it is unmaintainable and still proves nothing.
  • Avoid nested quantifiers, which open you to catastrophic backtracking.
  • Layer it: HTML attribute, JavaScript feedback, server-side check, confirmation email.
  • Only the confirmation email actually validates anything.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers