Battle-tested patterns to start from
| Target | Pattern |
|---|---|
| Email (pragmatic) | ^[^\s@]+@[^\s@]+\.[^\s@]+$ |
| IPv4 address | ^(\d{1,3}\.){3}\d{1,3}$ (validate octets ≤255 in code) |
| ISO date | ^\d{4}-\d{2}-\d{2}$ |
| Slug | ^[a-z0-9]+(?:-[a-z0-9]+)*$ |
| Hex color | ^#(?:[0-9a-fA-F]{3}){1,2}$ |
Treat these as starting points: the “perfect” email regex is a famous trap — RFC 5322 allows things no signup form should accept. Validate shape with a simple pattern and confirm deliverability by actually sending the email.
Capture groups: numbered, named, non-capturing
Parentheses do three jobs. (...) captures by number, (?<year>...) captures by name — far more maintainable in replacement strings ($<year>) and in code — and (?:...) groups without capturing, which keeps numbering stable and is marginally faster. A pattern littered with unnamed groups becomes write-only; naming the two or three groups you actually extract is the single highest-leverage readability habit in regex.
When the answer isn’t a bigger regex
Regular expressions match regular languages — they fundamentally cannot parse arbitrarily nested structures. If you’re matching HTML tags with their contents, balanced parentheses, or JSON substrings, the pattern that “almost works” will fail on real input forever. Reach for a real parser (DOMParser for HTML, JSON.parse for JSON) and use regex for what it excels at: tokens, line formats, log fields, and validation of flat shapes.
Performance habits for production patterns
Anchor everything you can (^...$ lets the engine fail fast), prefer character classes over alternation ([abc] beats (a|b|c)), and be precise instead of reaching for .* — greedy dot-star forces backtracking across the whole input on failure. If a pattern runs on user-supplied text on a server, test it here against pathological inputs (long strings of almost-matches) before deploying; ReDoS is just catastrophic backtracking weaponized.