Greedy vs Lazy Regex: Why Your Pattern Matches Too Much
A regex written to capture one quoted value captures everything from the first quote on the line to the last. A pattern meant to grab a single HTML tag swallows half the document. Almost every "matches too much" bug has the same cause — greedy quantifiers — and the fix most people reach for first is not the best one.
What greedy means
The quantifiers *, +, ? and {n,m} are greedy by default: they match as much as possible, then give characters back only if the rest of the pattern cannot otherwise match.
Take the text name="Ada" role="admin" and the pattern ".*". The first quote matches. Then .* consumes every remaining character to the end of the line. The final " has nothing left to match, so the engine backtracks one character at a time until it finds a quote — the last one on the line. The match is "Ada" role="admin".
Paste both into the regex tester and the highlighted match makes the behaviour obvious.
Lazy quantifiers match as little as possible
Adding ? after a quantifier makes it lazy: *?, +?, ??, {n,m}?. A lazy quantifier matches the minimum, then extends one character at a time only when the rest of the pattern fails.
With ".*?", the engine matches the opening quote, tries to match the closing quote immediately, fails, takes one character, tries again, and stops at the first closing quote. The result is "Ada", and with the global flag, a second match of "admin".
The better fix: say what cannot appear
Lazy quantifiers solve the immediate problem, but they still describe the content as "anything", and they still work by trial and error. A negated character class describes the content precisely instead:
"[^"]*"
This reads as: a quote, then any number of characters that are not quotes, then a quote. There is only one way for it to match, so it cannot overshoot, it does not depend on greedy or lazy behaviour, and it avoids the step-by-step extension a lazy quantifier performs. On long inputs that is noticeably faster.
The same idea applies widely:
- Text inside brackets:
\[[^\]]*\] - A value up to the next comma:
[^,]* - A path segment:
/[^/]+ - A single line only:
[^\n]*
When lazy is still the right tool
A negated class works when the delimiter is a single character. When the terminator is a multi-character sequence, you cannot express "not this sequence" in one character class, and a lazy quantifier is the practical choice. Matching an HTML comment is the classic example:
<!--[\s\S]*?-->
Here [\s\S] matches any character including newlines, and the lazy quantifier stops at the first -->. In engines that support it, the s (dotAll) flag lets .*? do the same job.
Laziness does not fix a wrong pattern
Lazy quantifiers find the shortest match from the first possible starting position — not the shortest match anywhere. The engine scans left to right and returns as soon as it succeeds from a starting point, so a lazy pattern can still produce a longer match than expected when an earlier position also succeeds.
For example, a.*?c on a a b c returns a a b c, not a b c, because a match beginning at the first a is found before the engine ever tries the second. If you need the innermost match, exclude the start delimiter from the content: a[^a]*?c.
Anchors and boundaries reduce surprises
Many overmatching problems disappear once the pattern states where a match must begin and end. ^ and $ anchor to the start and end of the input (or of each line with the multiline flag), and \b marks a word boundary so \bcat\b does not match inside "concatenate". Validation patterns in particular should almost always be anchored; without anchors, a pattern that checks an email address will happily accept one hidden inside a longer invalid string. That is part of why a dedicated email validator is more dependable than a pattern copied from a forum.
Applying it to a real edit
Suppose you need to remove every style="…" attribute from a template. The greedy pattern style=".*" deletes from the first style attribute to the last quote on the line, taking classes and ids with it. The precise pattern style="[^"]*" removes only the attributes. Test it against a sample in the regex tester, then run it across the whole file with regex mode in find and replace, and review the result before saving.
A quick checklist when a match is too long
- Find the
.*or.+in the pattern. - Replace it with a negated class of the closing delimiter where possible.
- Use a lazy quantifier when the terminator is more than one character.
- Add anchors or word boundaries where the match position is known.
- Test on input containing two or more candidate matches, not just one.