Regex Email Validation — Patterns, Edge Cases, and Best Practices
Email validation regex needs to balance strictness with real-world email formats. Learn why the perfect email regex is impossible, pragmatic patterns that work, RFC 5322...
Email validation regex is famously tricky — the RFC 5322 specification for valid email addresses is more complex than most developers realize. Here’s a pragmatic approach that catches obvious mistakes without blocking valid addresses.
Use the Regex Tester to test email validation patterns.
Why “perfect” email regex is impractical
The full RFC 5322 valid local-part can contain:
- Quoted strings:
"user name"@example.com - Comments:
user(comment)@example.com - IP addresses:
user@[192.168.1.1] - Internationalized domain names:
user@münchen.de
A regex that handles all of this is 6KB long and nearly unmaintainable. For practical use, reject the theoretical edge cases.
Pragmatic email regex
// Covers 99.9% of real email addresses:
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
EMAIL_REGEX.test('user@example.com') // true
EMAIL_REGEX.test('user.name+tag@example.co.uk') // true
EMAIL_REGEX.test('user@subdomain.example.com') // true
EMAIL_REGEX.test('@example.com') // false (no local part)
EMAIL_REGEX.test('user@') // false (no domain)
EMAIL_REGEX.test('user@.com') // false (domain starts with dot)
EMAIL_REGEX.test('user @example.com') // false (space)
EMAIL_REGEX.test('user@example.c') // false (TLD too short)
What it misses (intentional trade-offs)
// These are valid per RFC 5322 but rejected by the pragmatic regex:
'user name@example.com' // Space in unquoted local part
'"user name"@example.com' // Quoted local part
'user@[192.168.1.1]' // IP address literal
'user@localhost' // No TLD (valid in some internal systems)
// Usually you WANT to reject these for web forms
// Exception: if building email servers or strict RFC compliance
More permissive pattern (fewer false negatives)
// Looser — accepts more edge cases:
const LOOSE_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
LOOSE_EMAIL.test('user@example.com') // true
LOOSE_EMAIL.test('"user"@example.com') // true (quoted local part)
LOOSE_EMAIL.test('user@localhost') // true (no TLD required)
HTML5 email input validation
The browser’s built-in email validation uses a simplified RFC 5321 pattern:
<!-- Browser validates format on submit: -->
<input type="email" name="email" required>
<!-- Custom pattern: -->
<input type="email" name="email"
pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
title="Enter a valid email address">
The browser pattern is intentionally lenient to accommodate international addresses.
JavaScript form validation
function validateEmail(email) {
const regex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
if (!email) return { valid: false, error: 'Email is required' };
if (!regex.test(email)) return { valid: false, error: 'Invalid email format' };
if (email.length > 254) return { valid: false, error: 'Email too long' };
const [local, domain] = email.split('@');
if (local.length > 64) return { valid: false, error: 'Local part too long' };
return { valid: true };
}
Python email validation
import re
EMAIL_PATTERN = re.compile(
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
)
def is_valid_email(email: str) -> bool:
if not email or len(email) > 254:
return False
return bool(EMAIL_PATTERN.match(email))
# For production: use email-validator library
# pip install email-validator
from email_validator import validate_email, EmailNotValidError
def validate_email_address(email: str) -> str:
"""Validates and normalizes an email address."""
try:
# Validates format AND DNS MX record
valid = validate_email(email, check_deliverability=True)
return valid.email # Normalized email
except EmailNotValidError as e:
raise ValueError(str(e))
Server-side: the definitive check
Regex catches format errors. For definitive validation:
// Check DNS MX records (server-side only):
import dns from 'dns/promises';
async function isDeliverableEmail(email) {
const domain = email.split('@')[1];
try {
const records = await dns.resolveMx(domain);
return records.length > 0;
} catch {
return false; // Domain has no MX records
}
}
// But the ONLY definitive check: send a confirmation email
// If they click the link, the email is valid and they own it
Related tools
- Regex Tester — test email patterns
- Email Extractor Python — extract emails from text
- Regex Quantifiers Guide — *, +, ?, {n,m}
Related posts
- The 2026 Regex Cheatsheet (PCRE, JS, Python — Side by Side) — A dense reference for modern regex: anchors, character classes, quantifiers, loo…
- Catastrophic Backtracking: The Regex That Kills Your Server — One regex with nested quantifiers can reduce your server to 100% CPU in millisec…
- JavaScript Regex Flags — g, i, m, s, u, and v Explained — JavaScript regex flags change how patterns match. Learn when to use global /g, c…
- Email Extractor in Python — regex, html.parser, and BeautifulSoup — Extract email addresses from plain text, HTML pages, and files using Python. Thi…
- Regex Named Capture Groups — ?<name> Syntax and Use Cases — Named capture groups in regex use (?<name>...) syntax to give match groups reada…
Related tool
Test regular expressions with live match highlighting and explanation.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.