The email regex copied from a decade-old answer rejects o'[email protected], [email protected], [email protected] and [email protected]ña — real addresses belonging to real customers, who are told their email is “invalid” and cannot sign up.

A client-side email check has a modest job: catch obvious typing mistakes quickly and help the user correct them. Whether an address exists can only be answered by sending mail to it. This page, part of synchronous validation patterns, replaces overreaching patterns with a permissive rule, adds typo suggestions that catch the mistakes people actually make, and places real verification where it belongs.


Context and prerequisites

The address formats that strict patterns commonly reject, all valid and in use:

  • Plus addressing — [email protected] (Gmail, Fastmail, many providers).
  • Apostrophes and other punctuation in the local part — o'brien@, first.last@, a_b-c@.
  • Long and new TLDs — .museum, .photography, .london; patterns with {2,4} for the TLD fail these.
  • Internationalised domain names — correo.españa (sent over the wire as punycode xn--espaa-rta), and internationalised local parts under SMTPUTF8.
  • Subdomains — [email protected].

Meanwhile the mistakes users actually make are different: a missing @, a space, a trailing dot, a comma instead of a dot, and domain typos such as gmial.com or hotmail.con. A good client check targets those.

Real addresses against a typical strict pattern The address ada plus receipts at example dot com is rejected by a strict pattern that forbids plus signs and accepted by the permissive rule. o apostrophe brien at example dot com is rejected by the strict pattern and accepted permissively. user at example dot museum is rejected by a pattern limiting TLDs to four letters and accepted permissively. jose at correo dot españa is rejected by an ASCII-only pattern and accepted permissively. ada at example with no dot is accepted by both but flagged permissively as needing a domain ending. Address Strict pattern Permissive rule [email protected] rejected accepted o'[email protected] rejected accepted [email protected] rejected accepted [email protected]ña rejected accepted ada@example accepted asks for a domain ending

The core pattern: a permissive check plus typo suggestions

export type EmailCheck =
  | { ok: true; normalised: string; suggestion?: string }
  | { ok: false; message: string };

// One @, something before it, a dot somewhere after it with something on both
// sides, no whitespace. That is all a client can usefully require.
const SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@.]+$/u;

const COMMON_DOMAINS = ["gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "icloud.com", "aol.com", "proton.me"];

export function checkEmail(raw: string): EmailCheck {
  const value = raw.trim();
  if (value === "") return { ok: false, message: "Enter your email address." };
  if (/\s/.test(value)) return { ok: false, message: "Email addresses cannot contain spaces." };
  const at = value.split("@").length - 1;
  if (at === 0) return { ok: false, message: "Enter an email address with an @, like [email protected]." };
  if (at > 1) return { ok: false, message: "Enter an email address with only one @." };
  if (value.endsWith(".")) return { ok: false, message: "Email addresses cannot end with a dot." };
  if (!SHAPE.test(value)) return { ok: false, message: "Enter the part after the @, like example.com." };

  const [local, domain] = value.split("@");
  // Normalise ONLY the domain: domains are case-insensitive; local parts may not be.
  const normalised = `${local}@${domain.toLowerCase()}`;
  const suggestion = suggestDomain(domain.toLowerCase());
  return { ok: true, normalised, suggestion: suggestion ? `${local}@${suggestion}` : undefined };
}

// Suggest a common domain if the typed one is within edit distance 2 (and not identical).
function suggestDomain(domain: string): string | undefined {
  let best: { d: string; dist: number } | undefined;
  for (const d of COMMON_DOMAINS) {
    const dist = levenshtein(domain, d);
    if (dist > 0 && dist <= 2 && (!best || dist < best.dist)) best = { d, dist };
  }
  // Also catch wrong TLDs on common providers: gmail.con, hotmail.co
  const tldFix = domain.replace(/\.(con|cmo|comm|co)$/i, ".com");
  if (!best && tldFix !== domain && COMMON_DOMAINS.includes(tldFix)) return tldFix;
  return best?.d;
}

function levenshtein(a: string, b: string): number {
  const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
  for (let j = 1; j <= b.length; j++) dp[0][j] = j;
  for (let i = 1; i <= a.length; i++)
    for (let j = 1; j <= b.length; j++)
      dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
  return dp[a.length][b.length];
}

A suggestion is not an error: show “Did you mean [email protected]?” with a button to accept it, and let the user submit their original if it is correct.


Step-by-step walkthrough

  1. Use type="email" with autocomplete="email". Mobile keyboards show @ and ., and browsers autofill saved addresses — see autocomplete tokens for autofill-friendly forms.
  2. Check shape, not spec compliance. One @, a non-empty local part, a domain containing a dot, no whitespace. Specific messages for each common mistake are more useful than one generic “invalid”.
  3. Trim, and lowercase only the domain. Leading and trailing spaces from copy-paste are never intended. Local parts are technically case-sensitive; most providers ignore case, but you should not change what the user typed there.
  4. Suggest, never auto-correct. A typo suggestion is a question. Silently changing gmial.com to gmail.com would be wrong for the user whose domain really is gmial.com.
  5. Validate on blur. Email is typed in bursts; checking per keystroke shows “Enter the part after the @” while the user is still typing it. Use the timing in reward early, punish late.
  6. Confirm by sending mail. The only real validation is a confirmation link or code. Design the flow so an unverified address can be corrected easily.

Why “confirm your email” fields do not help

Asking users to type their email twice feels like it should catch typos, but people copy the first field into the second, or make the same slip twice, and the second field adds friction for everyone — especially on mobile and for people using switch access or voice input. A clear display of the entered address before submit (“We’ll send your receipt to [email protected] — change”), typo suggestions for common domains, and a confirmation email with an easy way to correct the address catch far more mistakes with less effort.

From typing to a verified address The input uses type email and autocomplete email so mobile keyboards and autofill help. On blur, the permissive check catches missing at signs, spaces and missing domain endings with specific messages. If the domain looks like a typo of a common provider, a suggestion is offered and the user may accept it. The server repeats the shape check and normalises the domain. A confirmation email proves the address works, with a way to change it if it does not arrive. Input attributes type=email, autocomplete=email Right keyboard; autofill fills most addresses correctly. Permissive check on blur @, domain ending, no spaces. Specific message per mistake. Typo suggestion gmial.com → gmail.com? A question with an Accept button, never an auto-correction. Confirmation email The only real verification. Easy to change the address if nothing arrives.

Failure modes and edge cases

1. The browser’s own type="email" validation

Native validation for type="email" uses a deliberately simple pattern defined in the HTML spec, which does not accept internationalised domains in Unicode form in every browser. With novalidate and your own rule you control this; without it, some browsers block [email protected]ña.

2. Disposable-domain blocklists

Blocking disposable email providers is a business decision with a cost: blocklists are incomplete and occasionally include legitimate providers. If you do it, do it on the server with a clear message, not as a client “invalid email”.

3. Uniqueness checks

“This email is already registered” is an async check, run after the shape passes — see implementing async email availability checks. Consider the privacy implication: it reveals which addresses have accounts.

4. Schema library defaults

z.string().email() and similar helpers use their own patterns, which have changed between versions and may reject some valid addresses. Test your library’s rule against the table above, and substitute a custom refine with the permissive check if needed.

5. Case-sensitive comparisons

If accounts are looked up by exact string match, [email protected] and [email protected] become two accounts. Normalise the domain everywhere, and decide deliberately whether to lowercase the local part for lookups.

What the client check is for The client-side check should catch missing at signs, spaces, missing domain endings and common domain typos quickly, with specific messages and suggestions. It should not try to decide whether an address exists, whether its mailbox accepts mail, or whether a domain is disposable; those need the server and a confirmation email. Client check does Missing @, spaces, trailing dot. Missing domain ending. Typo suggestions for common domains. Client check does not Decide the address exists. Decide the mailbox accepts mail. Police disposable domains.

Verification checklist


Frequently Asked Questions

Isn't there an official regex for email addresses?

The full RFC 5322 grammar allows quoted local parts, comments and other forms that no real user types, and a regex implementing it is enormous. The HTML specification defines a simpler “valid email address” pattern for type="email". Neither tells you whether an address works; only sending mail does.

Should I validate that the domain has MX records?

It is possible on the server with a DNS lookup and catches some typos, but DNS failures, slow resolvers and domains that accept mail without MX records (falling back to A records) cause false rejections. If you do it, treat failure as a warning, not a block.

Which common domains should the suggestion list include?

The ones your users actually use — check your existing sign-up data for the top domains and their frequent misspellings. A short, relevant list produces fewer wrong suggestions than a long generic one.


Related

← Synchronous Validation Patterns