What the engine is actually doing

A regular expression is not a search string. It is a small program, and the engine most languages use runs it by walking the text from left to right, trying each alternative in turn and backtracking when a path fails. Almost every regex mystery — why a pattern matches too much, why it is slow, why it hangs — comes from that backtracking. A tester is useful because it shows you the result, but the model is what lets you predict it.

The pieces, briefly

PieceMeans
.Any character except a newline
[abc] and [^abc]One of these, and one not of these
[0-9] or \dA digit
* + ?Zero or more, one or more, zero or one
{2,5}Between two and five
^ and $Start and end of the text
\bA word boundary — position, not a character
(...) and (?:...)Capturing group, non-capturing group
a|bEither alternative
(?=...) and (?!...)Lookahead: assert without consuming

The word boundary is worth singling out because it is the fix for the commonest false positive. Searching for cat finds it inside concatenate; searching for \bcat\b does not.

Greedy versus lazy, the concept that fixes most patterns

Quantifiers take as much as they can and only give characters back when forced. Given the text <b>bold</b> and <b>more</b>:

PatternMatches
<b>.*</b>The entire string, from the first tag to the last
<b>.*?</b>Each pair separately, as intended

The greedy version is not broken — it is doing exactly what it was told. Adding the question mark makes the quantifier lazy, so it takes the least it can and expands only as needed. If a pattern is capturing far more than you expected, this is nearly always why.

Flags, and the stateful one that bites

FlagEffect
iCase-insensitive
gFind all matches, not just the first
m^ and $ match at each line, not just the whole text
sThe dot also matches newlines
uTreat the pattern as Unicode code points

Two defaults surprise people. The dot does not match a newline unless you ask, so a pattern intended to grab a multi-line block silently stops at the first line ending. And ^ means start of the whole text, not start of a line, until you add the multiline flag — the source of countless patterns that work on one-line samples and fail on a real file.

The global flag has a trap of its own in JavaScript: a regex object with g keeps a position between calls, so testing the same pattern object against the same string repeatedly returns true, then false, then true. If a validation function alternates between accepting and rejecting identical input, this is the cause. Create the pattern where you use it, or drop the flag when you only need a yes or no.

Catastrophic backtracking, which has taken down real sites

Nested quantifiers over overlapping content make the number of paths the engine must try grow exponentially. The textbook example is (a+)+b tested against a run of the letter a with no b at the end. Every way of splitting the run between the inner and outer quantifier is a distinct path, and there are roughly two to the power of the length:

Letters of inputApproximate paths tried
10a thousand
20a million
30a billion
40a trillion

A twenty-character difference in input turns instant into forever. This is not a curiosity: it is a denial-of-service class in its own right, and it has caused well documented outages. A regular expression in a web application firewall rule took Cloudflare offline globally in July 2019 by consuming CPU across its fleet, and a pattern matching trailing whitespace on user posts took Stack Overflow down in July 2016. In both cases the pattern was correct and had passed review.

The defences, in order of usefulness: avoid a quantifier inside a quantified group; make alternatives mutually exclusive so there is nothing to redistribute; put an upper bound on repetition rather than using an open-ended plus; anchor the pattern so failure is detected early; and where the language offers atomic groups or possessive quantifiers, use them to forbid backtracking outright. Some environments provide an engine that cannot backtrack exponentially at all, which is the structural fix if you run untrusted patterns.

The testing habit that matters: try your pattern against a long input that almost matches but fails at the end. Happy-path samples never reveal this.

Two jobs regex should not be given

Email addresses. Regex is routinely recommended for this and it is a poor fit. The address grammar in the relevant standard permits quoted local parts, comments in parentheses and other structures that no readable pattern captures; the well known attempts at full compliance run to thousands of characters and still get edge cases wrong. Worse, strict patterns cause real harm by rejecting valid addresses — plus-addressing, apostrophes in names, long or newer top-level domains, non-ASCII local parts.

And even a perfect pattern cannot tell you whether the mailbox exists or accepts mail, which is the thing you actually wanted to know. The defensible approach is a loose sanity check such as ^[^@ ]+@[^@ ]+\.[^@ ]+$, or the browser email input type, followed by sending a confirmation message. Deliverability is verified by delivering.

Nested structures. HTML, XML and JSON nest to arbitrary depth, and a regular expression cannot count depth — this is a formal limitation of the class of languages regular expressions describe, not a matter of writing a cleverer pattern. Every regex-based HTML extractor works on the samples it was written against and breaks on attributes containing angle brackets, comments, or unexpected nesting. Use a parser. The same applies to CSV once fields can contain quoted commas and embedded line breaks.

A related note on passwords. Composition patterns that demand an uppercase letter, a digit and a symbol are easy to express as regex and have fallen out of favour for good reason — the 2017 revision of the United States federal digital identity guidance dropped mandatory composition rules, because they push users towards predictable substitutions rather than genuine unpredictability. Checking length and screening against a list of breached passwords does more than any pattern.

Flavours differ more than you expect

A pattern that works in one language may not compile in another. Lookbehind is unsupported or restricted in several engines; named group syntax differs; POSIX bracket classes exist in some flavours and not others; and the digit shorthand means ASCII digits only in some engines and any Unicode digit in others — a difference that matters for validation.

Unicode is its own hazard. Without the Unicode flag, the dot and the quantifiers operate on storage units rather than characters, so a single emoji counts as two and a pattern limiting input to a fixed length behaves unpredictably. Turning the flag on, and using grapheme-aware handling where user-visible characters matter, avoids a family of bugs that only appear once real users arrive.

Writing patterns you can still read later

Use named groups instead of counting positions. Break a long pattern into two or three simpler steps applied in sequence — one comprehensible pattern plus a line of code beats one unreadable pattern. Where the language supports an extended mode, use it to add whitespace and comments. And when a pattern exceeds about eighty characters, treat that as a signal that the job may belong to a parser.

Questions people actually ask

Why does my pattern match more than I wanted?

Greedy quantifiers. Add a question mark after the star or plus to make them lazy, and check whether you need a word boundary.

Why does my pattern hang the page?

Almost certainly nested quantifiers on overlapping input. Test with a long near-match, then remove the nesting or bound the repetition.

Why does the same test return true, then false?

A reused pattern object with the global flag remembering its position. Remove the flag or build the pattern fresh.

Are my pattern and test text sent to a server?

No. Matching runs in your browser and nothing is transmitted, logged or stored — which matters, since sample text pasted into a tester is often real production data.

Keep exploring Gen Code Tools

Every tool comes with a written guide, and every category is one click away.