Greedy and lazy quantifiers

Quantifiers look harmless. A + here, a * there — how hard can it be? Then you write a pattern that seems obviously correct, run it, and get a match that swallows half your string. The behavior that surprises you has a name: greediness. Once you understand it, quantifiers stop being a source of mystery.

If you only ever write pattern:/\d+/ you can coast on intuition. The moment you need something that reaches across delimiters, you have to know exactly how the engine walks the string.

A task that breaks the obvious pattern

You have some text and you want to swap every pair of straight quotes "..." for guillemets «...». Typographers in many languages prefer them.

So "Hello, world" should come out as «Hello, world». Other languages reach for different marks — „Witaj, świecie!” in Polish, 「你好,世界」 in Chinese — but guillemets are our target here.

Before you can replace anything, you have to find the quoted spans. The natural first guess is pattern:/".+"/g: a quote, then one-or-more of anything, then a closing quote. It reads correctly. It is also wrong.

let regexp = /".+"/g;

let str = 'a "cat" and a "fox" is shy';

alert( str.match(regexp) ); // "cat" and a "fox"

You wanted two matches, match:"cat" and match:"fox". You got one that stretches from the first quote all the way to the last: match:"cat" and a "fox".

what you wanted
a “cat” and a “fox” is shy
what /“.+”/ gave you
a “cat” and a “fox” is shy
Expected two matches, got one that spans both quoted words.

The culprit, in one phrase: greed.

How the greedy search actually runs

The engine’s top-level loop is short:

  • For every position in the string:
    • Try to match the whole pattern starting here.
    • No match? Move one position right and try again.

Stated that plainly, it doesn’t explain the failure. So let’s walk pattern:".+" character by character against subject:a "cat" and a "fox" is shy.

1. Find the opening quote. The first pattern token is pattern:". Position 0 holds subject:a, no match. The engine slides right until it finds a real quote at index 2.

a cat” and a “fox” is shy
Step 1 — the engine anchors on the first quote it can find.

2. Consume the first pattern:. The next token is a dot, meaning “any character except a line break”. The subject:c after the quote fits.

3. Repeat the dot — as far as it can go. Because pattern:.+ is greedy, the engine keeps eating characters. Every remaining character matches a dot, so it doesn’t stop until it hits the end of the string.

a cat” and a “fox” is shy
Step 3 — greedy .+ consumes everything to the end of the string.

4. Now look for the closing quote — and fail. After pattern:.+ the pattern still needs a pattern:". But the string is used up; there’s nothing left to match. The engine realizes it grabbed too much and begins to backtrack: it hands back one character from pattern:.+ and re-checks.

a cat” and a “fox” is shy
Step 4 — backtracking: give back the last character, test for a quote there.

The freed character is subject:y, not a quote. No match.

5–6. Keep giving characters back. The engine shortens pattern:.+ one step at a time, testing for a quote at each new boundary — subject:h, then subject:s, then a space, and so on — until the character just past pattern:.+ finally is a quote.

a cat” and a “fox is shy
Step 6 — backtracking stops at the last quote in the string.

7–8. Match complete. The first match is match:"cat" and a "fox". With the pattern:g flag the search resumes after it, but subject: is shy has no quotes, so nothing else turns up.

That is faithful to the rules — just not to your intent.

Lazy mode

Lazy mode is greedy mode’s mirror image. Its rule is “repeat as few times as possible”. You turn it on by placing a pattern:? right after a quantifier: pattern:*?, pattern:+?, and even pattern:?? for the pattern:? quantifier itself.

Swap in the lazy pattern:+? and the quote pattern behaves:

let regexp = /".+?"/g;

let str = 'a "cat" and a "fox" is shy';

alert( str.match(regexp) ); // "cat", "fox"

Here’s the same walk, now with the lazy quantifier.

1. Find the opening quote. Identical to before — the engine anchors on the quote at index 2.

2. Consume one pattern:. The subject:c matches the dot.

3. Try the rest of the pattern immediately. This is where laziness diverges. Instead of eating more characters, pattern:+? does the bare minimum and hands control to the next token, pattern:". The character sitting there is subject:a — not a quote — so this attempt fails.

a cat” and a “fox” is shy
Step 3 — lazy +? matches one character, then tests the closing quote right away.

4. Grudgingly take one more. Only because the rest of the pattern failed, the engine lets pattern:+? grow by one character and re-tests. Still no quote. Grow again. And again.

a cat” and a “fox” is shy
Step 4 — each failure forces exactly one more character into the match.

5. Stop at the first closing quote. The moment the next character is a quote, the rest of the pattern matches and the search ends.

a cat and a “fox” is shy
Step 5 — lazy mode settles on the NEAREST closing quote.

6. Resume and find the next. With pattern:g, the engine continues after match:"cat" and repeats the whole dance to produce match:"fox".

Here are the two modes side by side — same pattern skeleton, opposite starting instincts:

greedy .+
start at max →
give back 1 ←
give back 1 ←
give back 1 ← … until fit
lazy .+?
start at min →
take 1 more →
take 1 more →
take 1 more → … until fit
Greedy grabs the max then shrinks; lazy grabs the min then grows.

The takeaway for pattern:*? and pattern:?? is the same: each of them expands the repetition count only when the rest of the pattern cannot match at the current spot.

Reading about greed is one thing; watching it happen is another. Edit the pattern below and hit Run — the matches are highlighted right in the text. Start with ".+", then add one ? to make it ".+?" and see the highlight snap from a single sprawling match to two tidy ones.

interactiveWatch greedy vs lazy pick different matches

Laziness applies to one quantifier at a time

Adding pattern:? changes only the quantifier it’s attached to. Everything else in the pattern stays greedy.

alert( "705 82".match(/\d+ \d+?/) ); // 705 8

Trace it:

  1. pattern:\d+ is greedy, so it grabs every digit it can: match:705. It stops at the space, which isn’t a digit.
  2. The literal space in the pattern matches the space in the string.
  3. pattern:\d+? is lazy. It takes a single digit, match:8, then checks whether the rest of the pattern still needs anything.

There is nothing after pattern:\d+?. A lazy quantifier never repeats without being forced to, and nothing is forcing it. The pattern is satisfied at match:705 8.

705 82
greedy grabs all three digits  ·  lazy grabs one, then stops  ·  left untouched
One greedy quantifier and one lazy quantifier in the same pattern.

A different fix: exclusion instead of laziness

Regexes usually offer more than one route to the same result. The quotes task has a solution that never touches lazy mode: pattern:"[^"]+".

let regexp = /"[^"]+"/g;

let str = 'a "cat" and a "fox" is shy';

alert( str.match(regexp) ); // "cat", "fox"

Read it as: a quote, then one-or-more characters that are not a quote, then a closing quote. Because pattern:[^"] can never match a pattern:", the repetition is physically unable to cross a quote. It halts at the closing quote on its own — no backtracking needed.

a cat — [^“]+ stops here, blocked by the quote → fox is shy
[^"]+ is a wall: the repetition literally cannot include a quote character.

This is not a drop-in replacement for lazy quantifiers. It’s a genuinely different tool, and each has situations where it wins.

Where lazy mode fails but exclusion wins

Say you want to pull out button tags shaped like <button data-key="..." type="submit">, with any value in the data-key.

A first attempt with a greedy pattern:.*:

let str = '...<button data-key="save" type="submit">...';
let regexp = /<button data-key=".*" type="submit">/g;

// Works!
alert( str.match(regexp) ); // <button data-key="save" type="submit">

One button, works fine. Add a second button and the greed shows up again:

let str = '...<button data-key="save" type="submit">... <button data-key="load" type="submit">...';
let regexp = /<button data-key=".*" type="submit">/g;

// Whoops! Two buttons in one match!
alert( str.match(regexp) ); // <button data-key="save" type="submit">... <button data-key="load" type="submit">

Same disease as the quoted words: pattern:.* reached for the maximum and backtracked only to the last type="submit" in the string.

<button data-key="............................................" type="submit">
<button data-key="save" type="submit">... <button data-key="load" type="submit">

Make the quantifier lazy with pattern:.*?:

let str = '...<button data-key="save" type="submit">... <button data-key="load" type="submit">...';
let regexp = /<button data-key=".*?" type="submit">/g;

// Works!
alert( str.match(regexp) ); // <button data-key="save" type="submit">, <button data-key="load" type="submit">

Two clean matches. Looks solved:

<button data-key="....." type="submit">    <button data-key="....." type="submit">
<button data-key="save" type="submit">... <button data-key="load" type="submit">

Now feed it a nastier string:

let str = '...<button data-key="save" type="reset">... <input value="" type="submit">...';
let regexp = /<button data-key=".*?" type="submit">/g;

// Wrong match!
alert( str.match(regexp) ); // <button data-key="save" type="reset">... <input value="" type="submit">

Broken. The match runs past the button and swallows an pattern:<input...> tag. Why?

  1. The engine matches the start match:<button data-key=".
  2. pattern:.*? takes one character (lazily), then checks for pattern:" type="submit">. No luck.
  3. It takes one more, checks again, and repeats — expanding until it eventually finds match:" type="submit">.

The trouble is where that type="submit" lives. In this string the first button has type="reset", so the nearest type="submit" belongs to a completely different tag further along. Lazy mode dutifully walked right past the closing > of the button to reach it.

<button data-key="................................................" type="submit">
<button data-key="save" type="reset">... <input value="" type="submit">

Both the greedy and the lazy . have the same blind spot: a dot is happy to match pattern:" and pattern:>, so neither version respects the boundary of the data-key value. The fix is to forbid those characters. Use pattern:data-key="[^"]*" — grab everything up to the nearest quote, and nothing past it.

let str1 = '...<button data-key="save" type="reset">... <input value="" type="submit">...';
let str2 = '...<button data-key="save" type="submit">... <button data-key="load" type="submit">...';
let regexp = /<button data-key="[^"]*" type="submit">/g;

// Works!
alert( str1.match(regexp) ); // null, no matches, that's correct
alert( str2.match(regexp) ); // <button data-key="save" type="submit">, <button data-key="load" type="submit">

Run all three patterns against both strings at once below. Flip between the well-formed input and the tricky one, and notice that only the exclusion pattern stays correct in both cases — it reports two buttons for the clean string and zero for the malformed one.

interactiveGreedy vs lazy vs exclusion on real anchor tags

Summary

Quantifiers run in one of two modes.

Greedy (default)

The engine repeats the quantified token as many times as it can. pattern:\d+ eats every available digit. When it can’t take more (no more matching characters, or the string ends), it tries to match the rest of the pattern. If that fails, it backtracks — dropping repetitions one at a time — and retries.

Lazy

Switched on by a pattern:? after the quantifier. The engine attempts the rest of the pattern before each additional repetition, so it grows the match only when forced to.

Lazy mode is not a cure-all. When a pattern:. inside a lazy quantifier can match your delimiter, the match can still overshoot in the wrong direction. The sturdier answer is often a tuned greedy search with an explicit exclusion, like pattern:"[^"]+", where the character class itself enforces the boundary.