Most regex problems are solved by about twelve constructs. The rest of the syntax exists, and you can look it up on the rare day you need it.
The constructs worth memorising
| Pattern | Matches | Example |
|---|---|---|
. | Any character except newline | a.c → abc, a1c |
\d \w \s | Digit, word char, whitespace | \d\d → 42 |
\D \W \S | The negation of each | \S+ → a run of non-space |
[abc] | Any one of these | [aeiou] → a vowel |
[^abc] | Any one except these | [^0-9] → not a digit |
* + ? | 0+, 1+, 0 or 1 | ab+ → ab, abb |
{2,5} | Between 2 and 5 | \d{4} → a year |
^ $ | Start, end of string | ^abc$ → exactly abc |
\b | Word boundary | \bcat\b → cat, not category |
(…) | Capture group | (\d+)-(\d+) |
(?:…) | Group without capturing | cheaper when you do not need it |
a|b | Either | cat|dog |
That table solves validation, extraction, find-and-replace and log filtering — which is what people actually use regex for.
Greedy versus lazy, the bug you will write
Quantifiers are greedy by default: they take as much as they can and give back only when forced. This is almost never what you want when matching between delimiters.
Against <b>bold</b> and <b>more</b>, the pattern <b>.*</b> matches the entire string — from the first opening tag to the last closing tag, swallowing everything between.
Adding ? makes it lazy: <b>.*?</b> stops at the first closing tag and matches each pair separately.
A better habit than lazy quantifiers where it applies: use a negated class instead. <b>[^<]*</b> cannot overshoot at all, because it is incapable of crossing a <. It is also faster, since there is no backtracking.
The dot does not match newlines
By default . matches any character except a line break. On single-line input this never comes up; on a file it produces patterns that mysteriously stop working.
The fix is the s flag — "dotall" — which makes the dot match newlines too. Separately, the m flag changes ^ and $ to match at each line rather than only at the start and end of the whole string.
g— find all matches, not just the first.i— case insensitive.m—^and$match per line.s—.also matches newlines.
The g flag has a trap of its own in JavaScript: a regex with g keeps a lastIndex between calls, so reusing the same object across .test() calls returns alternating true and false on identical input. Create it fresh, or reset lastIndex.
What not to use regex for
Regex describes regular languages. Several things people reach for it constantly are not regular, and the attempt fails in ways that pass every test you thought to write.
- HTML and XML. Nesting is unbounded, and regex cannot count. It will work on your sample and break on real input.
- Email addresses. The RFC-correct pattern is thousands of characters. Check for an
@with something either side, then send a confirmation email — which is the only real validation anyway. - CSV. Quoted fields can contain the delimiter and even newlines. Use a parser; the reasons are in CSV to JSON: the five things that go wrong.
- Dates. A regex can check the shape but not whether 31 February exists.
Catastrophic backtracking
One failure mode is worth recognising because it takes a site down rather than returning a wrong answer.
Nested quantifiers over overlapping patterns — (a+)+$ being the canonical example — can take exponential time on input that nearly matches. Twenty characters can hang a thread for minutes. When the pattern comes from user input or runs against it, that is a denial-of-service vector with a name: ReDoS.
Avoid nesting a quantifier inside a quantified group, prefer negated character classes to .*, and anchor patterns where you can. If a pattern feels slow on long input, it is not slow — it is exponential, and it will eventually meet input that stops it entirely. Test in the Regex Tester against deliberately awkward strings, not only the ones you expect.
Frequently asked questions
What is the difference between greedy and lazy quantifiers?
Greedy takes as much as possible and backs off only when forced; lazy takes as little as possible. Matching between delimiters is where it bites — <code><b>.*</b></code> spans from the first opening tag to the last closing one, while <code>.*?</code> stops at the first.
Why does my pattern stop at the end of a line?
Because the dot does not match newlines by default. Add the s flag for dotall behaviour. Separately, the m flag makes ^ and $ match at each line rather than only at the start and end of the whole string.
Can I parse HTML with regex?
No, not reliably. HTML nesting is unbounded and regex cannot count, so any pattern works on your sample and breaks on real input. Use a parser. The same applies to CSV, where quoted fields can contain the delimiter and even line breaks.
Why does my regex return true then false on the same string?
Because it has the g flag and you are reusing the object. A global regex keeps lastIndex between calls, so consecutive .test() calls on identical input alternate. Create the regex fresh each time, or reset lastIndex to 0.
What is catastrophic backtracking?
A pattern with nested quantifiers over overlapping content — (a+)+$ is the classic — can take exponential time on input that nearly matches. Twenty characters can hang a thread for minutes. If the pattern touches user input, it is a denial-of-service risk.
How should I validate an email address?
Check for an @ with something either side, then send a confirmation email. The RFC-correct pattern runs to thousands of characters, and even a perfect one cannot tell you the address exists. Delivery is the only real validation.