The syntax that covers most cases
A regex is a pattern that describes a set of strings. Most characters match themselves; a small set are operators.
| Pattern | Matches |
|---|---|
. | Any character except newline |
\d \w \s | Digit, word character, whitespace. Capitalise to negate: \D \W \S |
[abc] | Any one of a, b, c. [^abc] for anything else. [a-z] for a range |
* + ? | Zero or more, one or more, zero or one |
{2,5} | Between two and five repetitions |
^ $ | Start and end of string, or of each line with the m flag |
\b | Word boundary - \bcat\b matches "cat" but not "category" |
(...) | Capture group, available afterwards as group 1, 2, 3 |
(?:...) | Group without capturing |
a|b | Either alternative |
That table covers the overwhelming majority of real patterns. Lookahead, backreferences and named groups are useful additions, not prerequisites.
Misunderstanding one: greedy quantifiers
Quantifiers are greedy. + and * consume as much as they possibly can, then hand characters back one at a time until the rest of the pattern succeeds.
Given <b>bold</b> and <i>italic</i>, the pattern <.+> matches the entire string. The .+ takes everything to the end, then backtracks just far enough to find a final > - which is the last one, not the first.
Adding ? makes a quantifier lazy: it takes as little as possible and expands only when forced. <.+?> matches <b> alone.
When a pattern matches far more than you expected, greediness is nearly always the cause. A more precise alternative is often better than laziness: <[^>]+> says "characters that are not a closing bracket", which is both clearer and faster because it cannot backtrack.
Misunderstanding two: matching is not validating
A regex finds a match anywhere in the string unless you anchor it. So \d{4} as a validity check for a four-digit PIN happily accepts abc1234xyz, because it contains four consecutive digits.
For validation you need ^ and $: ^\d{4}$ means the entire string is exactly four digits. This single omission is behind a large share of validation bugs, and it is worth checking every pattern used for validation for both anchors.
Be careful with the m flag here. With multiline enabled, ^ and $ match at line boundaries, so an anchored pattern accepts a string whose first line matches - which is exactly the wrong behaviour for validating a single value. In JavaScript, \A and \z do not exist, so the practical answer is not to use m when validating.
Misunderstanding three: catastrophic backtracking
Some patterns take exponential time on inputs that do not match. The classic shape is a quantifier inside a quantifier over overlapping character classes: (a+)+$.
Given a string of thirty as followed by an !, the engine has to try every way of dividing those thirty characters into groups before concluding there is no match. That is over a billion possibilities, and the process appears to hang.
When such a pattern runs against user input, it becomes a denial-of-service vulnerability known as ReDoS - one request with a carefully chosen string occupies a CPU core indefinitely. Real outages have been caused this way, including at Cloudflare and Stack Overflow.
The defences: avoid nesting quantifiers over overlapping classes; prefer specific character classes to .; anchor patterns so failure is detected early; and put a length limit on any input a regex is applied to. If you must run complex patterns on untrusted input, use an engine with linear-time guarantees, such as Rust's regex crate or Google's RE2.
Patterns worth having
# Trim whitespace at both ends
^\s+|\s+$
# Hex colour, three or six digits
^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
# ISO date, roughly validated
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
# Duplicated consecutive word
\b(\w+)\s+\1\b
# Quoted string, handling escaped quotes
"(?:[^"\\]|\\.)*"
# Split camelCase into words
(?<=[a-z])(?=[A-Z])
When not to use a regex
Knowing where regex is the wrong tool saves more time than any amount of syntax.
HTML and XML. Nesting is not a regular language, so no pattern handles it correctly. Comments, attributes containing angle brackets and unusual nesting all break it. Use a parser.
Email addresses. The full RFC 5322 grammar is enormous, and every short pattern rejects valid addresses - including ones with apostrophes, plus signs and new top-level domains. Check for a single @ with something either side, then send a confirmation email. That is the only real validation.
CSV. Quoted fields can contain commas and newlines. A regex that handles the common case will corrupt real data.
JSON and other structured formats. Use the parser. It exists, it is correct, and it is faster.
Anything where a simpler method works. includes(), startsWith() and split() are clearer to read and harder to get wrong. A regex used where a string method would do is a maintenance cost with no benefit.
Making patterns readable
The reputation for illegibility is mostly self-inflicted, and three habits fix most of it.
Comment the pattern. Many languages support an extended mode - /x in Perl and Python - that ignores whitespace and allows comments inside the pattern. JavaScript does not, so build the regex from named string fragments instead.
Name your groups. (?<year>\d{4}) read later is worth far more than match[1].
Write a test with the failing cases. Not just what should match, but what should not. Most regex bugs are false positives, and only a negative test catches them.
Testing patterns interactively against real sample text is the fastest way to learn, and the fastest way to debug. Our regex tester highlights matches and capture groups as you type.
Frequently asked questions
Yes. JavaScript, Python, Perl, Java, Go and PCRE share a core but differ at the edges - lookbehind support, atomic groups, possessive quantifiers and Unicode handling all vary. Patterns copied between languages usually work, with occasional adjustment.
Because a regex searches anywhere in the string by default. Anchor it with ^ at the start and $ at the end to require the whole string to match.
Only with care. Patterns with nested quantifiers can take exponential time on crafted input, which is a denial-of-service vector. Keep patterns simple, anchor them, and limit input length.
Not strictly. Any short pattern rejects valid addresses. Check for one @ with content on both sides, then verify by sending a confirmation message - which is the only check that proves the address works.