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
^Anchor to the start of the input.
([a-zA-Z0-9_\.\-])+Match one or more of ("a" to "z", "A" to "Z", "0" to "9", "_", ".", or "-").
@Match "@".
(([a-zA-Z\-])+\.)+Match one or more of (one or more of ("a" to "z", "A" to "Z", or "-"), then ".").
([a-zA-Z]{2,4})+Match one or more of (between 2 and 4 of "a" to "z" or "A" to "Z").
$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.