Regex for Email Validation: The Right Pattern (and Why Most Are Wrong)
Most email regex patterns are either too strict or too loose. Here's a practical pattern that works, plus common mistakes to avoid.
- regex
- validation
- javascript
title: "Regex for Email Validation: The Right Pattern (and Why Most Are Wrong)" description: "Most email regex patterns are either too strict or too loose. Here's a practical pattern that works, plus common mistakes to avoid." date: "2026-05-30" tags: ["regex", "validation", "email", "javascript"] relatedTool: "regex-tester" published: true
Email validation with regex is one of the most common — and most over-engineered — tasks in development. The fully RFC 5322–compliant pattern is thousands of characters long, no one should paste it into a codebase, and it still can't tell you whether an address actually receives mail. The other extreme — the two-character .+ pattern people crib from Stack Overflow — waves through obvious junk. Below is a practical middle ground: the pattern I'd ship in production, why it works, why the popular alternatives don't, and where regex should stop.
The Pattern That Works for 99.9% of Cases
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
Piece by piece:
^— start of string.[a-zA-Z0-9._%+-]+— one or more valid local-part characters (letters, digits, dot, underscore, percent, plus, hyphen).@— the literal@.[a-zA-Z0-9.-]+— one or more valid domain characters.\.— a literal dot separating domain and TLD.[a-zA-Z]{2,}— a TLD of at least two letters.$— end of string.
This is not RFC 5322 compliant. It rejects a handful of technically valid but exceedingly rare addresses — quoted local parts ("weird name"@example.com), IP-address domains (user@[192.168.1.1]), and internationalized domain names in their raw punycode form. In practice, rejecting those causes fewer support tickets than accepting the malformed input the RFC-loose patterns wave through. Ship the practical pattern, handle the 0.001% of exotic addresses on a per-report basis if they ever come up, and get on with your day.
Patterns That Are Too Loose
Common bad patterns and what they let through:
.+@.+ — accepts @domain (no local part), user@ (no domain), user@. (no TLD), and spaces in here@bad.com (embedded spaces). Basically anything with an @ in it.
\S+@\S+ — accepts user@domain (no TLD dot), user@.com (empty domain label), and anything with non-whitespace on both sides of an @. Only marginally better than .+@.+.
.+@.+\..+ — accepts user@-domain.com (leading hyphen), user@domain..com (empty label between dots), .leading@example.com (leading dot in local part). Looks reasonable at a glance, isn't.
The pattern of failure is the same in each: too permissive on what characters count and no structure enforced on the domain. .+ is doing too much work.
Patterns That Are Too Strict
The other failure mode — patterns that reject real, valid addresses users actually have:
- Requiring exactly one dot in the local part. Rejects
first.last.name@example.comand other multi-dot local parts that are perfectly valid. - Limiting the TLD to 2–4 characters. Rejects
.museum,.technology,.international, and every new generic TLD ICANN has approved since 2013. There are now hundreds of TLDs longer than four characters. - Disallowing
+in the local part. Rejects Gmail's plus-addressing (user+tag@gmail.com) that a huge number of users rely on for filtering. - Disallowing underscores. Rejects
valid_underscore@example.com, which is a legitimate character in the local part. - Disallowing leading or trailing dots in the local part. This one is actually correct to reject —
.leading@example.comis invalid per RFC — so keep this restriction.
The lesson: every restriction you add to look thorough will bounce some real user tomorrow. Regex is the wrong place to enforce policy; it should only enforce shape.
The Real Answer: Don't Over-Validate with Regex
Regex should be the first filter, not the only one. The strategy that actually works in production:
- Regex to catch obvious junk (
asdf,user@, empty strings, spaces). This is the pattern above. - Check the domain has a valid MX record if you need higher confidence before writing to your database. A quick DNS lookup rejects most typo domains (
gmial.com,hotmial.co) and disposable-mail providers if you keep a blocklist. - Send a verification email with a signed token before treating the address as belonging to a real user. This is the only step that actually confirms both existence and ownership.
No regex on Earth can tell you whether an address receives mail or whether the person typing it controls the inbox. Only a verification email can. Use regex to stop the truly broken input from touching your database — don't ask it to solve a problem it fundamentally can't.
Language-Specific Implementations
The same pattern in three common places you'll need it:
JavaScript / TypeScript:
function isValidEmail(email) {
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
}
Python:
import re
EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
def is_valid_email(email: str) -> bool:
return EMAIL_RE.match(email) is not None
SQL (MySQL / MariaDB — other dialects use slightly different syntax):
WHERE email REGEXP '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
The SQL version double-escapes the backslash because the pattern lives inside a string literal that also uses backslash as an escape character. Test with your specific database — PostgreSQL uses ~* for case-insensitive match, SQL Server has no native regex without CLR, and SQLite needs the re module loaded.
Test Your Pattern
Paste any regex pattern into the CodeScrub Regex Tester to see matches highlighted in real time with a plain-English explanation of what each part does. Try the practical pattern against this test string — half of these are valid, half aren't:
good@example.com
bad@
also.good+tag@domain.co.uk
missing@tld
spaces in@email.com
user@192.168.1.1
first.last@company.museum
@no-local-part.com
valid_underscore@test.org
Expected: four matches — rows 1, 3, 7, and 9 (good@example.com, also.good+tag@domain.co.uk, first.last@company.museum, valid_underscore@test.org). The other five all fail, each for a different reason worth understanding:
- Row 2 — no domain after the
@. - Row 4 — no dot between domain and TLD.
- Row 5 — an embedded space in the local part.
- Row 6 — an all-numeric TLD. The practical pattern rejects
user@192.168.1.1because its final label (1) isn't two-plus letters. The truly RFC-valid IP-address form isuser@[192.168.1.1]with square brackets, which almost no real user ever types, so this is a rejection worth taking. - Row 8 — no local part before the
@.
If your pattern accepts @no-local-part.com, it's too loose; if it rejects first.last@company.museum, it's too strict. That gap is where every over-engineered email regex I've ever debugged has lived.
Bottom line
Don't spend an afternoon tuning email regex. Use the practical pattern, verify against real inputs in the CodeScrub Regex Tester, send a verification email, and spend the freed hours on a problem regex actually can solve.