Regex Explainer

The email validation regex that hangs

A widely copy-pasted email pattern with nested quantifiers. It works on valid input and takes minutes on the wrong invalid input.

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

Match a whole string consisting of one or more of ("a" to "z", "A" to "Z", "0" to "9", "_", ".", or "-"), then "@", then one or more of (one or more of ("a" to "z", "A" to "Z", or "-"), then "."), then one or more of (between 2 and 4 of "a" to "z" or "A" to "Z").

Do not run this against untrusted input — see below.

What to check

  • Dangerous

    Catastrophic backtracking — this pattern can hang

    (([a-zA-Z\-])+\.)+

    A quantifier repeats a group that itself contains an unbounded quantifier over an overlapping character set. On input that ALMOST matches, the engine has exponentially many ways to split the text between the two quantifiers and must try all of them before it can report failure. A few dozen characters can take minutes of CPU, which makes this a denial-of-service vector if the pattern ever runs against untrusted input.

    Fix: Remove one level of repetition. Usually the inner quantifier alone is enough: (a+)+ means the same thing as a+. Where the nesting is genuinely needed, make the inner part non-overlapping or use an atomic group in an engine that has one — JavaScript does not.

  • Probably a bug

    Two adjacent quantifiers can match the same characters

    (([a-zA-Z\-])+\.)+([a-zA-Z]{2,4})+

    Both quantifiers accept the same characters, so any input can be divided between them in many ways. The engine tries each division before failing, which is quadratic or worse on non-matching input.

    Fix: Make the two parts match disjoint character sets, or combine them into one quantifier.

  • Note

    Anchors match string ends, not line ends

    Without the m flag, ^ and $ match only the very start and end of the input — not the start and end of each line. On multi-line input this is often not what was intended.

    Fix: Add the m flag if the pattern should apply per line.

  • Note

    4 numbered capture groups

    Numbered groups shift whenever a group is added or removed earlier in the pattern, silently breaking every index that referenced them.

    Fix: Name them: (?<year>\d{4}) reads better and cannot be renumbered.

Step by step

  1. ^

    Anchor to the start of the input.

  2. ([a-zA-Z0-9_\.\-])+

    Match one or more of ("a" to "z", "A" to "Z", "0" to "9", "_", ".", or "-").

  3. @

    Match "@".

  4. (([a-zA-Z\-])+\.)+

    Match one or more of (one or more of ("a" to "z", "A" to "Z", or "-"), then ".").

  5. ([a-zA-Z]{2,4})+

    Match one or more of (between 2 and 4 of "a" to "z" or "A" to "Z").

  6. $

    Anchor to the end of the input.

Details

Capture groups
4
Flags
None set

Check your own pattern

Paste one on the home page, or call the API or MCP server. This page is also available as markdown.

Related examples