The building blocks that cover 90% of real patterns
A regular expression describes a shape of text, not one exact string. Most practical patterns are built from a small set of pieces:
| Syntax | Meaning | Example |
|---|---|---|
. |
Any character except newline | a.c matches abc, axc |
\d \w \s |
Digit, word character, whitespace | \d{3} matches 123 |
[abc] |
Character class — any one of these | [aeiou] matches a vowel |
[^abc] |
Negated class — any character not listed | [^0-9] matches a non-digit |
* + ? |
Quantifiers — 0+, 1+, 0-or-1 | ab* matches a, ab, abbb |
{n,m} |
Between n and m repetitions | \d{2,4} matches 2–4 digits |
^ $ |
Start / end of string (or line, with a flag) | ^Hello matches only at the start |
(...) |
Capturing group | (\d{3})-(\d{4}) captures two groups |
| |
Alternation (OR) | cat|dog matches either word |
A basic email-shape pattern, ^[\w.-]+@[\w.-]+\.\w+$, combines several of these: one or more word/dot/hyphen characters, an @, another word/dot/hyphen run, a literal dot, then the TLD.
Greedy vs. lazy quantifiers — the most common surprise
By default, quantifiers are greedy — they match as much as possible, then backtrack only if needed to let the rest of the pattern succeed. This produces unexpected results with repeated delimiters:
Input: <b>bold</b> and <i>italic</i>
Pattern: <.+>
Greedy match: <b>bold</b> and <i>italic</i> (matches everything between the FIRST < and the LAST >)
Lazy match (<.+?>): <b> and separately </b>, <i>, </i> (matches the shortest possible span each time)
Adding ? after a quantifier (+?, *?) makes it lazy — match as little as possible instead. This single character is the fix for the extremely common "my regex matched way more than I expected" bug when parsing anything with repeated delimiters, like HTML tags or quoted strings.
Catastrophic backtracking: when a pattern hangs the process
Certain patterns can go exponential on the wrong input — most commonly, nested quantifiers like (a+)+ or (a|a)+ applied to a string that almost, but doesn't quite, match. The engine tries every possible way of splitting the repeated group across the input before giving up, and the number of ways grows exponentially with input length. A pattern that runs instantly on a 20-character string can take longer than the age of the universe on a 40-character string of the wrong shape — this is a real, exploitable denial-of-service vector (ReDoS) if user input ever reaches an unvalidated regex like this. The fix is almost always restructuring the pattern to avoid nested/ambiguous repetition, not just hoping the input stays short.
Capturing groups vs. non-capturing groups
(...) captures the matched text for later reference (via $1, \1, or a match array, depending on language); (?:...) groups for the purposes of applying a quantifier or alternation without capturing. Overusing capturing groups when you don't actually need the captured value adds overhead and clutters match results — a habit worth breaking once patterns get complex.
Common mistakes
- Forgetting greedy is the default, leading to over-matching across repeated delimiters — reach for lazy quantifiers (
+?,*?) or a more specific character class instead of.. - Using
.when you mean "not this specific character.".matches almost anything; a negated class like[^"]is usually more precise and avoids accidentally consuming past an intended boundary. - Writing nested quantifiers on user-controlled patterns, opening the door to catastrophic backtracking (ReDoS) on adversarial input.
- Forgetting to escape regex metacharacters when matching a literal. Characters like
.,*,(,)need escaping (\.,\*, etc.) to match themselves literally — a raw user string dropped into a regex without escaping can behave unpredictably.
FAQ
What's the difference between greedy and lazy quantifiers?
Greedy (*, +) matches as much as possible before backtracking; lazy (*?, +?) matches as little as possible — this matters most with repeated delimiters, where greedy can span far more text than intended.
What is catastrophic backtracking?
A performance blowup caused by ambiguous nested quantifiers (like (a+)+) where the regex engine tries exponentially many ways to split the match — a real security risk (ReDoS) if the pattern runs on untrusted input.
Should I always use capturing groups?
No — use non-capturing groups ((?:...)) when you only need grouping for a quantifier or alternation and don't actually need the matched substring afterward.
Is regex the right tool for parsing HTML?
Generally no for anything beyond trivial, well-known patterns — HTML's nested, irregular structure isn't a regular language, and a proper HTML parser handles edge cases (malformed tags, nesting, comments) far more reliably.
Build and test patterns against real sample text with the Regex Tester — matches highlight live, entirely in your browser.