Skip to content
AZ Tools

Regular Expressions in Practice

Regular expressions have a reputation for being write-only: easy to produce, hard to read back, and quietly wrong in ways that only show up on real data. Most of that comes from a small number of misunderstandings about what the engine is doing. This guide walks through them, and ends with the cases where the right move is to put the pattern away.

What the engine is actually doing

A regular expression is not a description of the string you want — it is a small program the engine runs at every position in the input, left to right. It tries to match starting at index 0; if that fails it moves to index 1 and tries again, and so on until something matches or the input runs out. Almost every surprising result follows from that scan.

It is why `\d{3}` happily reports a match on "abc1234def": it found "123" in the middle, and nothing in the pattern said otherwise. "Contains three digits" and "is exactly three digits" are different questions, and the pattern only answers the one you asked.

Anchors and word boundaries

`^` and `$` tie the pattern to the start and end of the input — or, with the multiline flag, to the start and end of each line, which is a quiet source of over-permissive validators. `\b` is a zero-width position between a word character and a non-word character, so `\bcat\b` finds the animal in "the cat sat" but not the letters inside "concatenate".

For validation, anchor the whole pattern and test the values you want rejected, not just the ones you want accepted. A validator that lets everything good through but also lets garbage through is worse than no validator: it moves the failure downstream, to a place where the bad value is much harder to trace back.

Greedy, lazy, and catastrophic backtracking

Quantifiers are greedy by default: `.*` consumes as much as it can and only hands characters back when the rest of the pattern fails. Adding `?` makes them lazy. Against `<b>one</b> and <b>two</b>`, the pattern `<.*>` swallows the entire line, while `<.*?>` stops at the first `>` — the same intent, a completely different result.

That handing-back is also where performance dies. Nested quantifiers over overlapping character sets — `(a+)+b` is the textbook case — can push the engine into trying an exponential number of ways to split the input before it concedes there is no match. On attacker-supplied text that is a denial-of-service bug, usually called ReDoS. Keep quantified groups from overlapping, and prefer a specific character class over `.` wherever you can.

Character classes, the dot, and Unicode

`.` means "any character except a newline" unless you set the dotall flag, and `\d`, `\w` and `\s` are ASCII-centric in most flavours: `\w` excludes accented letters, and `\d` may or may not include other scripts' digits depending on the engine. For text that is not plain ASCII, turn on Unicode mode and reach for property escapes such as `\p{L}` (any letter) or `\p{N}` (any number).

Ranges inside a class are code-point ranges, which is why `[A-z]` silently includes the six punctuation characters that sit between `Z` and `a`. Write the class you mean. And remember that the same-looking text can be different sequences: "é" as a single code point does not match a pattern built for "e" followed by a combining accent, so normalize before you compare.

When a regex is the wrong tool

Nested and quoted formats — HTML, JSON, CSV, source code — are not regular languages. A pattern can nibble at them, but every quoted delimiter and every level of nesting adds another special case, until the expression is unreadable and still wrong on the input you have not seen yet. Use a real parser: for CSV that means one that honours quoting, for HTML it means the DOM.

Email addresses are the other classic trap. The grammar in the specification is far larger than the pattern people paste from the internet, so strict-looking regexes routinely reject valid addresses — including perfectly ordinary ones with a plus sign or a long new top-level domain. Check that there is an `@` with something plausible either side, then prove the address the only way that works: send mail to it.

  • Match at the position you mean: anchor with `^`…`$` for validation, leave it unanchored for search.
  • Prefer `[^"]*` over `.*?` when you know the delimiter — it is clearer and it cannot backtrack.
  • Test with the inputs you expect to reject, not only the ones you expect to accept.
  • If the format nests or quotes, use a parser.

Related tools