Toolmingo
Guides6 min read

How to Validate an Email Address (Regex, Code Examples & Common Mistakes)

Learn how to validate email addresses correctly in JavaScript, Python, Go, and PHP. Understand what regex can and can't check, common pitfalls, and when to go beyond syntax validation.

Email validation is one of those tasks that looks simple but has hidden depth. There is no perfect regex that catches every invalid email while accepting every valid one — but there are practical patterns that work well for the 99.9% of real-world inputs you will encounter. This guide explains what email validation can and cannot do, gives you code in four languages, and covers the mistakes that cause subtle bugs.

What makes an email address valid?

An email address has the format local@domain. The full specification (RFC 5322) allows an astonishing range of formats — quoted strings, comments in parentheses, even spaces inside quotes. In practice, almost all real-world email addresses follow a much simpler subset:

  • Local part: letters, digits, dots, hyphens, underscores, plus signs — no consecutive dots, no leading or trailing dot
  • @ symbol: exactly one, mandatory
  • Domain: letters, digits, hyphens, at least one dot, a TLD of two or more characters

A pattern that covers this practical subset will accept every email your users will realistically type, while rejecting obvious typos.

The validation layers

There are three distinct levels of email validation, each catching different problems:

Layer What it checks Example caught
Syntax (regex) Format looks like an email user@ (no domain)
DNS lookup Domain exists, has MX records user@thisdoesnotexist.xyz
SMTP probe Mailbox accepts connections nonexistent@gmail.com

For most web forms, syntax validation is sufficient. DNS and SMTP checks add latency and complexity; use them only when delivery failures are very costly (transactional email, account confirmation flows).

A practical regex pattern

This pattern covers the practical subset of valid emails:

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

Breaking it down:

Part Pattern Meaning
Local part [a-zA-Z0-9._%+\-]+ One or more: letters, digits, . _ % + -
Separator @ Literal at-sign
Domain [a-zA-Z0-9.\-]+ Letters, digits, dots, hyphens
Dot \. Literal dot before TLD
TLD [a-zA-Z]{2,} Two or more letters (covers .io, .museum, .photography)
Anchors ^ $ Must match the entire string

This pattern intentionally rejects edge cases like IP-literal addresses (user@[192.168.1.1]) and quoted local parts ("user name"@example.com). These are technically valid per RFC 5322 but are never used in practice.

Code examples

JavaScript

// Browser and Node — covers async form validation
function isValidEmail(email) {
  if (typeof email !== 'string') return false;
  const trimmed = email.trim().toLowerCase();
  const pattern = /^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$/;
  return pattern.test(trimmed);
}

// Usage
console.log(isValidEmail('alice@example.com'));    // true
console.log(isValidEmail('alice@example'));         // false — no TLD
console.log(isValidEmail('alice @example.com'));    // false — space
console.log(isValidEmail('alice+filter@sub.io'));   // true — plus addressing

// HTML5 also provides built-in validation:
// <input type="email"> — browser handles basic format check
// Access programmatically: input.validity.valid

Python

import re

EMAIL_PATTERN = re.compile(
    r'^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$',
    re.IGNORECASE
)

def is_valid_email(email: str) -> bool:
    if not isinstance(email, str):
        return False
    return bool(EMAIL_PATTERN.match(email.strip()))

# Usage
print(is_valid_email('alice@example.com'))        # True
print(is_valid_email('alice@example'))             # False
print(is_valid_email('alice..double@example.com')) # True — pattern allows it
                                                   # add extra check if needed

# For stricter validation, the email-validator package is battle-tested:
# pip install email-validator
# from email_validator import validate_email, EmailNotValidError
# try:
#     valid = validate_email("alice@example.com")
#     email = valid.email  # normalised form
# except EmailNotValidError as e:
#     print(str(e))

Go

package main

import (
    "regexp"
    "strings"
)

var emailPattern = regexp.MustCompile(
    `(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`,
)

func isValidEmail(email string) bool {
    trimmed := strings.TrimSpace(email)
    return emailPattern.MatchString(trimmed)
}

func main() {
    tests := []string{
        "alice@example.com",      // valid
        "alice+tag@sub.example.io", // valid — plus and subdomain
        "alice@example",          // invalid — no TLD
        "@example.com",           // invalid — empty local part
        "alice @example.com",     // invalid — space
    }
    for _, email := range tests {
        println(email, "->", isValidEmail(email))
    }
}

PHP

<?php

function isValidEmail(string $email): bool {
    $trimmed = trim($email);
    
    // PHP has a built-in filter — use it as the first check
    if (filter_var($trimmed, FILTER_VALIDATE_EMAIL) === false) {
        return false;
    }
    
    // filter_var handles RFC 5321 syntax; add TLD length check
    $domain = substr(strrchr($trimmed, '@'), 1);
    $tld = substr(strrchr($domain, '.'), 1);
    return strlen($tld) >= 2;
}

// Usage
var_dump(isValidEmail('alice@example.com'));    // bool(true)
var_dump(isValidEmail('alice@example'));         // bool(false)
var_dump(isValidEmail('alice @example.com'));    // bool(false) — space

// For DNS validation (optional):
// $domain = substr(strrchr($email, '@'), 1);
// $hasMX = checkdnsrr($domain, 'MX');

What regex cannot validate

Even a perfect regex cannot detect these problems:

  • Domain does not existuser@thisdoesnotexist12345.com passes any syntax check
  • Mailbox does not existnobody@gmail.com is syntactically valid but unreachable
  • Disposable emailabc@mailinator.com is valid but often used for throwaway signups
  • Typos in real domainsuser@gmial.com looks valid to any regex

For catching typos in popular domains, libraries like mailcheck.js suggest corrections ("Did you mean gmail.com?"). For blocking disposable addresses, maintain or subscribe to a blocklist.

Quick reference: common patterns by use case

Use case Recommended approach
Web form syntax check Regex pattern above or type="email" in HTML
Python backend email-validator package
PHP backend FILTER_VALIDATE_EMAIL filter
Confirm delivery is possible DNS MX record lookup
Block disposable addresses Blocklist (e.g. disposable-email-domains)
Confirm the inbox exists Send a confirmation email — only reliable method

6 common mistakes

1. Not trimming whitespace. Users often paste email addresses with a leading or trailing space. Always trim() / strip() before validating. The examples above all do this.

2. Case-sensitive comparison. The domain part of an email is case-insensitive by spec. Store and compare emails lowercased to prevent Alice@Example.com and alice@example.com being treated as different accounts.

3. Trusting validation as deliverability proof. A valid format does not mean the email will be delivered. Build your registration flow to handle bounces.

4. Blocking legitimate emails with over-strict regex. Patterns that forbid + signs break plus-addressing (Alice uses alice+shopping@gmail.com to filter). Patterns that require exactly one dot in the domain break .io, .co, and similar TLDs. Use the permissive pattern above rather than inventing your own restrictive one.

5. Validating only on the client. Always re-validate on the server. Client-side validation is a UX convenience; it is not a security control.

6. Not re-validating before sending. If your system stores emails and sends to them later, re-validate the format before each send. Data quality degrades over time through manual edits, imports, and migrations.

FAQ

Q: What is the maximum length of an email address? RFC 5321 sets the maximum at 254 characters total (64 for the local part, 255 for the domain). In practice, check that the value fits in your database column and reject anything over 254 characters.

Q: Can I use HTML <input type="email"> instead of writing regex? Yes, for client-side UX. Browsers implement their own email validation for type="email" inputs, which covers the common cases. You still need server-side validation — never rely solely on client-side checks.

Q: Should I send a verification email or validate with DNS? For critical flows (account creation, newsletter signup), always send a verification email. It is the only way to confirm that a human controls the inbox. DNS validation is a useful extra step but not a substitute for email confirmation.

Q: My regex rejects user.name+tag@example.co.uk — what's wrong? The plus sign (+) and the two-dot TLD (.co.uk) are both valid. Make sure your character class includes + in the local part ([a-zA-Z0-9._%+\-]+) and that your domain pattern allows multiple dots ([a-zA-Z0-9.\-]+).

Q: Is user@localhost a valid email? Technically yes per RFC 5321, for sending mail on a local network. For public web forms, you almost certainly want to reject it. Add a check that the domain contains at least one dot.

Q: How do I test an email validation regex interactively? Use Toolmingo's Regex Tester — paste the pattern and a list of test addresses, and see which match instantly in your browser with no data sent to a server.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools