Regex

Email Validation Regex: The Complete Guide for 2026

2026-05-31 · 9 min read · Test your regex free →

Validating an email address with a regular expression is the most-viewed topic on Stack Overflow — the top question alone has over 3.5 million views. Yet most of the popular answers are outdated, overly complex, or produce patterns that incorrectly reject real email addresses. This guide gives you the patterns that work in 2026, explains every component, shows correct implementations in JavaScript, Python, React, and Django, and tells you when to go beyond regex entirely.

Why Email Regex Gets So Many Views

The full specification for a valid email address (RFC 5322) is extraordinarily complex. A complete regex satisfying it runs over 6,000 characters. No developer wants that, and no production system needs it. The actual goal when validating email in a web form is pragmatic: catch obvious typos before submission, ensure a plausible format, and prevent empty or clearly broken inputs from reaching your backend. The pattern that covers 99%+ of real-world email addresses while remaining readable and maintainable is far simpler.

The Best Practical Email Regex for 2026

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

Component breakdown

Pattern partWhat it matchesWhy included
^Start of stringPrevents matching an email embedded in a longer string
[a-zA-Z0-9._%+\-]+Local part (before @)All common characters in email local parts. The + handles Gmail-style aliases like user+tag@gmail.com
@The @ symbolRequired in every valid email address
[a-zA-Z0-9.\-]+Domain nameLetters, digits, dots (for subdomains), hyphens
\.A literal dotEscaped because dot is a metacharacter meaning "any character"
[a-zA-Z]{2,}Top-level domainMinimum 2 letters, no upper limit — handles .com through .international
$End of stringPrevents partial matching — the entire string must be an email

JavaScript Implementation

// Basic validation function
function isValidEmail(email) {
  if (!email || typeof email !== 'string') return false;
  const pattern = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
  return pattern.test(email.trim().toLowerCase());
}

// Test cases
console.log(isValidEmail('user@example.com'));              // true
console.log(isValidEmail('user.name+tag@sub.example.co.uk')); // true
console.log(isValidEmail('user@example.museum'));           // true (long TLD)
console.log(isValidEmail('invalid'));                       // false
console.log(isValidEmail('@nodomain.com'));                 // false
console.log(isValidEmail('no@tld'));                       // false
console.log(isValidEmail('spaces here@example.com'));      // false
console.log(isValidEmail(''));                              // false

// In a form validation handler
document.querySelector('form').addEventListener('submit', (e) => {
  const email = document.querySelector('[name="email"]').value;
  if (!isValidEmail(email)) {
    e.preventDefault();
    showError('Please enter a valid email address');
  }
});

Performance note for high-volume use

// Compile the regex once outside the function for performance
// Avoids re-compilation on every call in tight loops
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;

function isValidEmail(email) {
  return typeof email === 'string' && EMAIL_REGEX.test(email.trim().toLowerCase());
}

// Validate 10,000 emails efficiently
const results = emailList.map(isValidEmail);

Python Implementation

import re

# Compile once, reuse many times
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 not isinstance(email, str):
        return False
    return bool(EMAIL_PATTERN.match(email.strip().lower()))

# Test cases
test_cases = [
    ('user@example.com', True),
    ('user.name@example.com', True),
    ('user+tag@example.com', True),
    ('user@sub.example.co.uk', True),
    ('invalid', False),
    ('@nodomain.com', False),
    ('no@tld', False),
    ('', False),
]

for email, expected in test_cases:
    result = is_valid_email(email)
    status = '✓' if result == expected else '✗ UNEXPECTED'
    print(f'  {status} {email!r}: {result}')

HTML5 Native Validation — The Simplest Option

Before adding JavaScript, check whether HTML5 native validation covers your requirements. Adding type="email" to an input element activates browser-native validation that catches missing @, missing domain, and obvious format errors — with zero JavaScript code.

<!-- HTML5 email validation — no JavaScript needed -->
<input type="email" name="email" required placeholder="you@example.com">

<!-- With additional pattern constraint -->
<input
  type="email"
  name="email"
  required
  pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
  title="Enter a valid email address (e.g. you@example.com)"
  autocomplete="email"
>

The limitation: HTML5 validation runs only in the browser and is trivially bypassed with a direct HTTP request or curl. Always validate server-side as well. Browser validation is a UX improvement — it gives users immediate feedback — not a security control.

React — Real-Time Validation with User Experience Best Practices

import { useState, useCallback } from 'react';

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

function EmailField({ onValidChange }) {
  const [value, setValue] = useState('');
  const [dirty, setDirty] = useState(false);  // only show error after user interaction

  const isValid = EMAIL_REGEX.test(value.trim());
  const showError = dirty && value.length > 0 && !isValid;

  const handleChange = useCallback((e) => {
    const newVal = e.target.value;
    setValue(newVal);
    onValidChange(EMAIL_REGEX.test(newVal.trim()));
  }, [onValidChange]);

  return (
    <div style={{ marginBottom: 16 }}>
      <label style={{ display: 'block', marginBottom: 6, fontSize: 14 }}>
        Email address
      </label>
      <input
        type="email"
        value={value}
        onChange={handleChange}
        onBlur={() => setDirty(true)}
        placeholder="you@example.com"
        autoComplete="email"
        style={{
          width: '100%',
          padding: '8px 12px',
          borderRadius: 6,
          border: `1px solid ${showError ? '#f87171' : '#374151'}`,
          background: '#111',
          color: '#e4e4e7',
          fontSize: 15,
        }}
        aria-invalid={showError}
        aria-describedby={showError ? 'email-error' : undefined}
      />
      {showError && (
        <p id="email-error" style={{ color: '#f87171', fontSize: 13, marginTop: 4 }}>
          Please enter a valid email address
        </p>
      )}
    </div>
  );
}

Express.js and Node.js Server-Side Validation

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

// Middleware for validating email in request body
function validateEmail(req, res, next) {
  const { email } = req.body;

  if (!email || typeof email !== 'string') {
    return res.status(400).json({ error: 'Email is required' });
  }

  const normalised = email.trim().toLowerCase();
  if (!EMAIL_REGEX.test(normalised)) {
    return res.status(400).json({ error: 'Invalid email address format' });
  }

  req.normalisedEmail = normalised;  // pass to next handler
  next();
}

// Use in routes
app.post('/register', validateEmail, async (req, res) => {
  const email = req.normalisedEmail;
  // email is now validated and normalised
  await createUser({ email });
  res.status(201).json({ message: 'User created' });
});

Django

from django.core.validators import validate_email
from django.core.exceptions import ValidationError
import re

EMAIL_REGEX = re.compile(
    r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
)

def clean_email(email: str) -> str:
    """Validate and normalise an email address. Raises ValueError if invalid."""
    if not email or not isinstance(email, str):
        raise ValueError('Email is required')
    email = email.strip().lower()
    try:
        validate_email(email)  # Use Django's battle-tested built-in validator
        return email
    except ValidationError as e:
        raise ValueError(f'Invalid email: {e.message}')

# In a Django form
class RegistrationForm(forms.Form):
    email = forms.EmailField(
        widget=forms.EmailInput(attrs={
            'placeholder': 'you@example.com',
            'autocomplete': 'email',
        })
    )  # Django's EmailField uses its own validator automatically

Validating Deliverability With DNS MX Record Check

Regex validates format — not whether the domain can actually receive email. An address like real.user@doesnotexist-xyz123.com passes any format regex but bounces immediately when you try to send to it. For applications where deliverability matters — registration flows, mailing list signups — add an MX record check after format validation.

import re
import dns.resolver  # pip install dnspython

EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')

def has_mx_records(domain: str) -> bool:
    try:
        answers = dns.resolver.resolve(domain, 'MX', lifetime=3.0)
        return len(answers) > 0
    except Exception:
        return False

def validate_email_with_dns(email: str) -> dict:
    email = email.strip().lower()

    if not EMAIL_REGEX.match(email):
        return {'valid': False, 'reason': 'Invalid format'}

    domain = email.split('@')[1]
    if not has_mx_records(domain):
        return {'valid': False, 'reason': 'Domain cannot receive email'}

    return {'valid': True, 'email': email}

# Usage
result = validate_email_with_dns('user@gmail.com')
print(result)  # {'valid': True, 'email': 'user@gmail.com'}

Performance: DNS lookups add 50–300ms per check. Run them asynchronously or in a background job after form submission, not synchronously in a blocking request handler where the user is waiting for a response.

Test Suite — Run These Before Deploying

// JavaScript test suite
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;

const VALID = [
  'user@example.com',
  'user.name@example.com',
  'user+tag@example.com',       // Gmail-style alias
  'user@sub.example.com',       // subdomain
  'user@example.co.uk',         // multi-segment TLD
  'user@example.museum',        // long TLD
  '123@example.com',            // digits in local part
  'user_name@example.com',      // underscore
  'user-name@example.com',      // hyphen in local part
  'USER@EXAMPLE.COM',           // uppercase (case-insensitive in practice)
];

const INVALID = [
  '',
  'notanemail',
  '@nodomain.com',              // missing local part
  'nodomain@',                  // missing domain
  'no@tld',                     // TLD too short (1 char)
  'double@@example.com',        // two @ signs
  'spaces in@example.com',      // space in local part
  'user@',                      // empty domain
  'user@.com',                  // dot at start of domain
];

let passed = 0, failed = 0;
VALID.forEach(e => {
  if (EMAIL_REGEX.test(e)) { passed++; }
  else { console.error('Should be VALID but failed:', e); failed++; }
});
INVALID.forEach(e => {
  if (!EMAIL_REGEX.test(e)) { passed++; }
  else { console.error('Should be INVALID but passed:', e); failed++; }
});
console.log(`${passed} passed, ${failed} failed`);

Paste any of these test cases into ToolPry's Regex Tester to see match results with live highlighting as you adjust the pattern.

What This Regex Does Not Validate

The practical pattern does not validate: quoted local parts with spaces ("user name"@example.com is RFC 5321-valid but essentially unused in practice), IP address domain literals (user@[192.168.1.1]), internationalized email addresses with non-ASCII characters, or whether the specific mailbox exists. For internationalized domain names (IDN), use a dedicated library like idna in Python. For mailbox existence, the only reliable check is sending a verification email and confirming the link is clicked.

Frequently Asked Questions

Should I use regex or an email validation library?

For simple browser-side format validation in a web form, a well-tested regex is fine. For backend production validation where security, internationalisation, or deliverability matter, use a dedicated library. In JavaScript, validator.js has an isEmail() function tested against comprehensive edge cases. In Python, email-validator combines format checking and DNS verification in one call. In Django, the built-in EmailValidator handles the common cases. Libraries are updated when standards change — a regex you write today may reject valid addresses as new TLDs are introduced.

Why does my regex reject addresses it should accept?

Most common causes: TLD length restriction too strict (if you use [a-zA-Z]{2,6} instead of [a-zA-Z]{2,}, you reject TLDs longer than 6 characters like .international), the + sign missing from the local part character class (rejecting user+tag@gmail.com), or the pattern is case-sensitive and rejecting uppercase addresses. Always normalise to lowercase before testing, and use {2,} with no upper limit for the TLD.

Must I store email addresses in lowercase?

Yes. The domain part is case-insensitive by convention — USER@EXAMPLE.COM and user@example.com reach the same mailbox. The local part is technically case-sensitive per the RFC but virtually all real mail servers treat it as case-insensitive. Always normalise with email.trim().toLowerCase() before storing to prevent duplicate accounts and ensure consistent database lookups.

Using Email Regex in Node.js API Validation Middleware

For REST APIs built with Express or Fastify, email validation belongs in middleware so it runs consistently across all routes that accept email input. This keeps validation logic in one place and ensures every endpoint normalises the email to lowercase before it reaches business logic or database queries.

// middleware/validateEmail.js
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;

module.exports = function validateEmailMiddleware(req, res, next) {
  const raw = req.body?.email;
  if (!raw || typeof raw !== 'string') {
    return res.status(400).json({ error: 'email is required' });
  }
  const email = raw.trim().toLowerCase();
  if (!EMAIL_REGEX.test(email)) {
    return res.status(400).json({
      error: 'Invalid email address',
      field: 'email',
      value: email,
    });
  }
  req.body.email = email;  // normalised
  next();
};

// router.js
const validateEmail = require('./middleware/validateEmail');
router.post('/subscribe', validateEmail, subscribeHandler);
router.post('/login', validateEmail, loginHandler);

Email Validation in TypeScript with Type Guards

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

// Type guard: narrows string to a branded type for compile-time safety
type Email = string & { readonly __brand: 'Email' };

function isEmail(value: string): value is Email {
  return EMAIL_REGEX.test(value.trim().toLowerCase());
}

function parseEmail(value: string): Email {
  const normalised = value.trim().toLowerCase();
  if (!isEmail(normalised)) {
    throw new Error(`Invalid email address: ${value}`);
  }
  return normalised as Email;
}

// Usage
const email = parseEmail('USER@EXAMPLE.COM');  // returns 'user@example.com' as Email type
// TypeScript now knows this variable holds a valid email

Frequently Asked Questions

Should I use regex or an email validation library?

For simple browser-side format checking in a web form, a well-tested regex is appropriate. For backend production systems where security, internationalisation, or deliverability matter, use a dedicated library. In JavaScript, validator.js provides a battle-tested isEmail() function. In Python, email-validator combines format validation with DNS checking in a single call. In Django, the built-in EmailValidator handles common cases. Libraries are maintained when standards evolve — a regex you write today may reject valid addresses as new TLDs are registered.

Why does my email regex reject valid addresses?

The most common causes: TLD length restriction set too tightly — using [a-zA-Z]{2,6} instead of [a-zA-Z]{2,} rejects long TLDs like .international or .academy. The + sign missing from the local part character class — this rejects user+tag@gmail.com, a widely used pattern. A case-sensitive pattern rejecting uppercase addresses — always normalise to lowercase before testing. Missing the hyphen in the character class — the hyphen must be first or last inside [] to be treated as a literal, not a range operator.

Do email addresses need to be stored case-sensitively?

No — always normalise to lowercase before storing. The domain part of an email is defined as case-insensitive. The local part is technically case-sensitive per RFC 5321, but virtually every real mail server in production treats it as case-insensitive. User@Example.com, user@example.com, and USER@EXAMPLE.COM all reach the same mailbox on all major email providers. Storing without normalisation causes duplicate account problems and inconsistent lookup results. Always apply email.trim().toLowerCase() before any database write or comparison.

How do I validate multiple emails at once?

// JavaScript — validate comma-separated emails
function validateEmailList(input) {
  const emails = input.split(',').map(e => e.trim()).filter(Boolean);
  const regex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
  const valid = [];
  const invalid = [];
  for (const email of emails) {
    (regex.test(email.toLowerCase()) ? valid : invalid).push(email);
  }
  return { valid, invalid };
}

const result = validateEmailList('a@b.com, invalid, c@d.org');
// { valid: ['a@b.com', 'c@d.org'], invalid: ['invalid'] }

Use ToolPry's Regex Tester to paste your pattern and test multiple email addresses simultaneously with live match highlighting before deploying to production.

Handling Edge Cases That Break Common Regex Patterns

Even a well-crafted email regex will encounter inputs that expose its weaknesses. Understanding these edge cases helps you decide whether your pattern is adequate for your use case or whether you need a library.

The first common edge case is the subaddress or plus alias. Gmail, Fastmail, and many other providers support addresses like user+newsletter@gmail.com — the part after the plus sign is ignored for delivery but is technically part of the local part. Any email regex that excludes the + character from the local part character class will incorrectly reject these addresses, which are widely used for email filtering and tracking.

The second edge case is dots in unexpected positions. RFC 5321 allows multiple consecutive dots in the local part (user..name@example.com) and a dot immediately before the @ sign (user.@example.com). Most real mail servers reject these, but the strict RFC allows them. Unless you are specifically targeting RFC 5321 compliance, treating these as invalid is a sensible practical choice — virtually no real user has such an address.

The third case is new and long TLDs. ICANN has approved hundreds of new generic TLDs in recent years: .academy, .software, .international, .photography. Any pattern that limits the TLD length with an upper bound — such as [a-zA-Z]{2,6} — incorrectly rejects these valid addresses. The pattern [a-zA-Z]{2,} with no upper bound handles all current and future TLDs correctly.

The fourth edge case is internationalized domain names. Domain names can now use non-ASCII characters through Punycode encoding — münchen.de encodes to xn--mnchen-3ya.de. Standard ASCII-only email regex handles the encoded Punycode form correctly but not the original Unicode representation. If your users include people whose domains use non-Latin scripts, use a library that handles IDN: Python's email-validator does this natively.

When Not to Use Regex for Email Validation

There are scenarios where regex validation alone is insufficient and can create user experience problems. The most important is during account recovery. If a user mistyped their email during registration and later cannot log in, your validation preventing them from entering the same typo during recovery actually helps — but blocking a valid corrected email because your regex has a bug creates a support burden. Err toward permissive validation on recovery flows.

The second scenario is B2B applications where users enter corporate email addresses. Some companies use non-standard local part characters, internal TLDs on private domains, or email routing through gateways that modify addresses. In these cases, after initial format validation, the most reliable approach is simply sending a confirmation email and requiring the user to click a link — this proves the address exists and is reachable, which no regex can determine.

For any application where deliverability matters more than format — mailing lists, transactional notifications, account verification flows — the only reliable validation is sending a confirmation email. Regex catches typos; it cannot catch mistyped-but-valid addresses like user@gamil.com or user@hotmial.com. Libraries that perform typo detection and suggest corrections (did you mean gmail.com?) provide better user experience than regex alone for catching these common mistakes.

Frequently Asked Questions

Should I use regex or an email validation library?

For simple browser-side format checking in a web form, a well-tested regex is appropriate and sufficient. For backend production systems where security, internationalisation, or deliverability matter, use a dedicated library. In JavaScript, validator.js provides an isEmail() function tested against comprehensive edge cases. In Python, email-validator combines format validation with optional DNS MX checking in a single call. In Django, the built-in EmailValidator handles the common cases reliably. Libraries are maintained when email standards evolve — a regex written today may reject valid addresses as new TLDs are delegated by ICANN.

Why does my email regex reject addresses it should accept?

The most common causes are: a TLD length restriction set too tightly — using [a-zA-Z]{2,6} instead of [a-zA-Z]{2,} rejects TLDs longer than 6 characters like .international. The + sign missing from the local part character class — this rejects user+tag@gmail.com, a widely used pattern for email filtering. A case-sensitive pattern rejecting uppercase addresses — always normalise to lowercase before testing. Or a hyphen misplaced inside a character class creating an unintended range — the hyphen must appear first or last inside [] to be literal, not between two characters.

Must email addresses be stored in lowercase?

Yes, always normalise to lowercase before storing. The domain part is defined as case-insensitive by the DNS specification. The local part is technically case-sensitive per RFC 5321 but every major mail provider — Gmail, Outlook, Yahoo, Apple — treats it as case-insensitive in practice. Storing mixed-case emails causes duplicate account problems and inconsistent database lookups. Always apply email.trim().toLowerCase() before any database write or equality comparison.

How can I suggest a corrected email when a user makes a typo?

Use a library that performs domain typo detection. In JavaScript, mailcheck.js compares the entered domain against a list of known providers and suggests corrections for common typos like gamil.comgmail.com or hotmial.comhotmail.com. In Python, the email-validator package has similar functionality. Show the suggestion as a non-blocking prompt — "Did you mean user@gmail.com?" — rather than blocking form submission, since the correction might not always be right.