Greedy vs Lazy Regex: The Quantifier Mistake Everyone Makes Once
There's a specific regex bug almost every developer writes once, gets confused by, and never forgets: a pattern built to grab one small piece of text instead swallows half the document. It's not a typo and it's not a broken character class - it's the quantifiers behaving exactly as specified, just not the way the author assumed. Understanding greedy versus lazy matching is the fix, and it's one of the few regex concepts that's genuinely worth internalizing rather than looking up every time.
What "Greedy" Actually Means
Quantifiers like * (zero or more), +
(one or more), and {n,} (n or more) are greedy by default in
JavaScript and nearly every other regex flavor. Greedy means the engine consumes as much input as it possibly
can while still being able to match, and only gives characters back - "backtracks" - if the rest of the pattern
can't succeed with that much consumed. It doesn't stop early just because a valid match was already found
earlier in the string; it keeps eating until the whole remaining pattern forces it to stop, or fails, and then
backs off one character at a time until the rest of the pattern is satisfied.
Here's the classic broken example: matching <.+> against the
string <b>bold</b>. The obvious intent is to match a
single HTML tag - you'd expect it to grab <b> and stop. But
.+ is greedy, so it first tries to consume the entire rest of the
string, then backtracks one character at a time only until it finds the last possible
> that lets the whole pattern still match. The result is the
match spans the entire input - <b>bold</b> in full -
not the single opening tag anyone actually wanted. The pattern isn't malformed; it did precisely what a greedy
quantifier is defined to do.
Lazy Quantifiers: Match as Little as Possible First
Adding a ? immediately after a quantifier flips it to lazy (also
called "non-greedy" or "reluctant") matching: *?,
+?, {n,}?. A lazy
quantifier consumes the minimum it can get away with, then expands one character at a time only if the rest of
the pattern fails to match with less. Rewriting the tag pattern as <.+?>
changes the outcome completely: it grabs the shortest possible span that still satisfies
<...>, which is exactly <b>.
Run it with the global flag against the same string and it now correctly finds two separate matches -
<b> and </b>
- instead of one match that swallowed everything in between.
You can see this difference directly using the Regex Tester
on this site: paste <b>bold</b> as the test string,
try <.+> with the Global (g) flag checked, then try
<.+?> with the same flag. The match count and boundaries
shown in the results panel change from one match spanning the whole string to two matches, each exactly one
tag long - the clearest possible demonstration of what "backtrack minimally versus backtrack maximally" means
in practice, without having to reason about it in the abstract.
A Second, Even More Common Gotcha: The Unescaped Dot
Greedy-versus-lazy gets the most attention, but there's a mistake that's arguably even more frequent and
usually more damaging: forgetting that . matches any single
character, not a literal period. Twelve characters carry special meaning in regex and need a backslash to
be matched literally: . * + ? ^ $ { } ( ) | [ ], plus the
backslash itself. The dot is the one people forget most often because it looks so unremarkable sitting in a
pattern that's clearly meant to match a version number or an IP address.
Write 192.168.1.1 as a pattern without escaping the dots, intending
to match that literal IP address, and it will also match 192x168x1x1,
192!168!1!1, or any string with the same digit layout separated by
any character at all - because each unescaped . means "any
character here," not "a period here." The correct pattern is 192\.168\.1\.1.
This is exactly the kind of mistake that passes casual testing - most of your test strings probably do contain
real periods in the right spots - and then quietly over-matches on real-world input months later. Test any
pattern with a deliberately "wrong but similarly shaped" string (swap a literal dot for another character) in
the Regex Tester before trusting it; if it still matches, something in your pattern isn't as literal as you
think it is.
A Detail Specific to This Tool: No Global Means First Match Only
Both gotchas above are general regex facts, true anywhere you write a pattern. But there's a behavior specific
to how the Regex Tester
is built that trips people up constantly, including experienced developers: without the Global (g) checkbox
ticked, clicking "Test" only returns the first match in the string, even if your test text clearly
contains several. This isn't a limitation unique to this tool - it's faithfully replicating how JavaScript's
own RegExp.exec() behaves without the global flag, which stops at
the first match by design. But because it's invisible in the UI (nothing tells you a second match exists unless
you go looking), it's an easy thing to blame on the pattern itself. If the tester reports "1 match" and you
expected three, check the Global checkbox before you start debugging the pattern.
Worth knowing too: the tester compiles patterns with JavaScript's native RegExp
engine specifically, not PCRE or Python's re - so inline flags
like (?i) from a Python pattern won't work here (use the flag
checkboxes instead), and the checkboxes only expose four of JavaScript's six flags - g, i, m, and s. There's no
checkbox for u (Unicode mode, needed for \p{...}
Unicode property escapes) or y (sticky mode), so a pattern relying on either will need testing
elsewhere.
Putting It Together
When a pattern matches more text than you expected, the fix is almost always one of two things: make the
quantifier lazy by adding a ?, or tighten the character class so
it physically can't cross the boundary you don't want crossed (for instance, [^>]+
instead of .+, which can't consume a >
no matter how greedy it is). When a pattern matches text you didn't expect at all, check for an unescaped
metacharacter - the dot most of all. Both are fast to verify by pasting the pattern and a couple of
deliberately tricky test strings into the
Regex Tester before
shipping it anywhere that matters.
