Regex Non-Greedy vs Greedy Matching — Side-by-Side Examples
One of the most common regex surprises is a quantifier matching way more text than expected. It usually comes down to greedy versus non-greedy (lazy) behavior. This guide shows exactly how each one works with side-by-side match results so you can predict the behavior instead of guessing.
Greedy Quantifiers (Default Behavior)
By default, quantifiers like *, +, and {n,m} are greedy: they try to consume as much of the string as possible first, then backtrack one character at a time until the rest of the pattern can match.
const html = '<b>bold</b> and <i>italic</i>'; // Greedy: .* grabs everything up to the LAST '>' html.match(/<.*>/)[0]; // '<b>bold</b> and <i>italic</i>' — spans the whole string!
Non-Greedy (Lazy) Quantifiers
Adding a ? after a quantifier makes it lazy: it matches as few characters as possible, only expanding when required for the overall pattern to succeed.
const html = '<b>bold</b> and <i>italic</i>'; // Non-greedy: .*? stops at the FIRST '>' html.match(/<.*?>/)[0]; // '<b>' — stops as soon as possible // With the global flag, you get every tag separately html.match(/<.*?>/g); // ['<b>', '</b>', '<i>', '</i>']
Side-by-Side Comparison Table
Input: "aaa" Pattern: a+ (greedy) → matches "aaa" (all three a's) Pattern: a+? (lazy) → matches "a" (just one a) Input: "start...middle...end" Pattern: start(.*)end (greedy) → captures "...middle...end" minus "end" itself... actually captures everything up to the LAST "end" Pattern: start(.*?)end (lazy) → captures "..." up to the FIRST "end" Input: '"first" and "second"' Pattern: ".*" (greedy) → matches '"first" and "second"' (whole thing) Pattern: ".*?" (lazy) → matches '"first"' (just the first quoted string)
All the Lazy Variants
*?— zero or more, as few as possible+?— one or more, as few as possible??— zero or one, prefers zero{n,m}?— between n and m, prefers n
A Better Alternative — Negated Character Classes
For extracting content between two known delimiters, a negated character class is often more precise and faster than a lazy dot, because it can't accidentally "jump over" a closing delimiter of a different kind:
const html = '<b>bold</b>'; // Lazy dot — works, but .*? can still match across unintended characters html.match(/<(.*?)>/)[1]; // 'b' // Negated character class — explicitly "anything but >" html.match(/<([^>]*)>/)[1]; // 'b' — same result here, but more robust on messy input
[^>]* is generally the preferred idiom over .*? when you know the exact character that should stop the match — it is both clearer intent and avoids some pathological backtracking cases that lazy dots can trigger on malformed input.
When to Choose Which
- Use greedy when you want the longest possible match, e.g. matching an entire multi-line comment block
- Use lazy when you want the shortest possible match between two known delimiters, e.g. quoted strings, single HTML tags
- Prefer a negated character class (
[^x]*) over a lazy dot when the stopping character is known and fixed - Always test against your actual data — greedy vs lazy mistakes are one of the most common sources of subtle bugs in text-processing code
Frequently Asked Questions
A greedy quantifier like .* matches as many characters as possible, then backtracks if needed. A non-greedy (lazy) quantifier like .*? matches as few characters as possible, expanding only if the rest of the pattern requires it.
Add a question mark after the quantifier: *? for zero-or-more, +? for one-or-more, ?? for zero-or-one, and {n,m}? for a bounded range.
Not necessarily. Performance depends on the input and how much backtracking each approach needs. In many cases a well-anchored non-greedy pattern is actually faster because it stops as soon as a match is found, rather than consuming the whole string and backtracking.