Lazy quantifiers: *?, +?, ?? and {n,m}?
Adding ? to any quantifier makes it lazy: it matches as few repetitions as it can and expands only when the rest of the pattern fails. The match position is unchanged, only its length.
At a glance
- Syntax
- *? +? ?? and {n,m}?, one trailing question mark
- Default
- every quantifier is greedy until you add the question mark
- Changes
- the length of the match, not where the match starts
- Best alternative
- a negated character class, when the terminator is a single character
- Performance
- still backtracks, so lazy is not the same thing as safe
- In this tester
- the explain tab labels each quantifier as greedy or lazy as you type
In detail
A greedy quantifier takes as much as it can and hands characters back one at a time until the rest of the pattern fits. A lazy one starts with the minimum and takes one more only when forced. The leftmost position where a match can start is the same either way, so laziness never finds a match that greed would have missed; it only changes how much text ends up inside it.
The textbook case is a pair of delimiters. Against the string "<a><b>", the greedy /<.+>/ matches the whole thing, because the dot happily eats the intervening angle brackets and the final one still satisfies the pattern. The lazy /<.+?>/ stops at the first closing bracket and returns "<a>", which is what the author wanted.
The better fix is usually a negated class. /<[^>]+>/ matches the same text without any backtracking at all, because the class simply cannot cross the terminator. Lazy quantifiers still backtrack, one character at a time, so on a long string with no terminator they do a great deal of work before concluding there is no match.
Laziness earns its place when the terminator is more than one character. There is no single character class for a closing HTML comment or a triple quote, so a lazy quantifier bounded by an explicit terminator is the practical spelling. Even then, keeping it bounded with {0,500}? rather than *? puts a ceiling on the damage.
Examples
"<a><b>".match(/<.+>/)[0]; // "<a><b>"
"<a><b>".match(/<.+?>/)[0]; // "<a>"
"aaa".match(/a+?/)[0]; // "a"
"aaaa".match(/a{2,4}?/)[0]; // "aa"'"a" and "b"'.match(/".*?"/)[0]; // '"a"'
'"a" and "b"'.match(/"[^"]*"/)[0]; // '"a"'Same result, but the second pattern cannot cross a quote in the first place, so there is nothing to undo.
Things that catch people out
- Lazy is not a performance fix. A lazy quantifier that fails still backtracks through every length, which on a long string with no terminator is exactly as slow as the greedy version.
- The question mark must follow the quantifier. /a?+/ is a syntax error rather than a possessive quantifier, because JavaScript has no possessive forms.
- Making a quantifier lazy does not stop it crossing a newline. That depends on whether the token before the quantifier can match one, not on the quantifier itself.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →The * quantifier: zero or more, including nothing at all
The star quantifier succeeds even when it matches nothing, which is why a global match against it returns an empty string at almost every position.
[^abc]: negated character sets that quietly cross newlines
A negated class matches one character that is not in the set, including line terminators. That is why a quoted string pattern runs across lines.
Catastrophic backtracking: why a regex freezes a page
Nested quantifiers over overlapping characters make the work grow exponentially with input length. Here is the shape that causes it and the rewrites for it.
Atomic groups in JavaScript: emulating them with a lookahead
JavaScript has no (?>...) and no possessive quantifiers. The lookahead and backreference pair gives you the same no backtracking behaviour.
The ^ anchor: start of string, or start of line
Where the caret matches with and without the m flag, why it consumes nothing, and the entirely different job it does inside a character class.
The $ anchor: end of string, and the trailing newline question
Where $ matches in JavaScript, why it refuses to match before a trailing newline when Python and Ruby allow it, and how to write a literal dollar sign.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links