Skip to content
RegexBuilder

Regex Email Validation: Patterns, Limitations, and Best Practices

Learn email validation regex patterns in Python, JavaScript, PHP, and SQL. Understand why regex can't fully validate emails and what to use instead.

By Editorial Team Updated
  • regex
  • email validation
  • python
  • javascript
  • form validation
Regex Email Validation: Patterns, Limitations, and Best Practices

Email validation with regex is one of the most common regex tasks — and one of the most often done wrong. This guide covers practical patterns and the important caveats.

The short version

Use a simple regex to catch obvious non-emails, then verify with a real email send (or use a validation API). No regex fully implements RFC 5322, and you don’t need it to.

Simple validation patterns

Most practical pattern (covers 99.9% of real email addresses):

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

Breaking it down:

  • ^ — start of string
  • [a-zA-Z0-9._%+-]+ — local part: one or more allowed characters
  • @ — literal @
  • [a-zA-Z0-9.-]+ — domain: one or more allowed characters
  • \. — literal dot
  • [a-zA-Z]{2,} — TLD: two or more letters
  • $ — end of string

Implementation in Python:

import re

def is_valid_email(email: str) -> bool:
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

print(is_valid_email("user@example.com"))      # True
print(is_valid_email("user+tag@sub.example.com")) # True
print(is_valid_email("invalid@"))              # False
print(is_valid_email("@nodomain.com"))         # False
print(is_valid_email("plain"))                 # False

Implementation in JavaScript:

function isValidEmail(email) {
  const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return pattern.test(email);
}

console.log(isValidEmail("user@example.com"));       // true
console.log(isValidEmail("user+tag@sub.domain.io")); // true
console.log(isValidEmail("notanemail"));             // false

HTML5 input validation (browser built-in):

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

The browser validates the format before submission. No JavaScript required for basic validation.

More permissive patterns

The simple pattern above rejects some technically valid email addresses. A more permissive pattern:

^[^\s@]+@[^\s@]+\.[^\s@]+$

This allows anything except spaces and @ in each part, which is closer to the actual RFC but also allows some invalid formats.

HTML5 pattern attribute

For form validation with a 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"
  required
/>

What regex can’t catch

Regex can verify format but not existence. These all pass the pattern above but are invalid for sending:

  • user@example.com — valid format, but does this mailbox exist?
  • test@thisdoesnotexist.com — valid format, domain may not exist
  • a@b.c — valid format, but .c TLD doesn’t exist
  • user@localhost — valid for local use but no regex for public email

What regex catches:

  • Missing @ symbol
  • Multiple @ symbols
  • Missing domain
  • Missing TLD
  • Spaces in the address

What regex doesn’t catch:

  • Nonexistent domain
  • Nonexistent mailbox
  • Disposable email addresses
  • Typos (gnail.com, yaoo.com)
  • Recently invalid formats that look valid

Best practices for email validation

Layer 1: Format check (regex) — catches obvious nonsense

def basic_format_check(email: str) -> bool:
    return bool(re.match(
        r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
        email
    ))

Layer 2: Domain MX record check — verifies the domain can receive email

import dns.resolver

def mx_record_exists(domain: str) -> bool:
    try:
        dns.resolver.resolve(domain, 'MX')
        return True
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.exception.Timeout):
        return False

def validate_email_domain(email: str) -> bool:
    if not basic_format_check(email):
        return False
    domain = email.split('@')[1]
    return mx_record_exists(domain)

Layer 3: Confirmation email — the only way to confirm the address is real and accessible to the user

Send a confirmation email with a unique link. If they click it, the email is valid. This is the only true validation.

Server-side validation

Never rely solely on client-side regex. Always validate on the server:

# Django
from django.core.validators import validate_email
from django.core.exceptions import ValidationError

def validate(email):
    try:
        validate_email(email)
        return True
    except ValidationError:
        return False
// Node.js: validator.js
const validator = require('validator');
validator.isEmail('user@example.com');  // true

Test your email regex patterns at regexbuilder.io.