completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Quantifiers: greedy & lazy

Learn JavaScript regex quantifiers, greedy and lazy matching, empty-match traps, and practical patterns for quoted strings, PINs, and cleanup.

By the end, you can
  • 01
    Pick the right repeat markUse +, *, ?, {n}, {n,}, and {n,m}, and know which ones can match nothing.
  • 02
    Follow greedy and lazy matchesSay how a greedy match takes the most and gives some back, and how a lazy match grows only as much as it must.
  • 03
    Avoid quantifier trapsSpot empty matches, quoted text that runs too far, length checks without ^ and $, and repeated capture groups.

Say how many times something repeats

A regular expression, or regex, is a pattern for finding text. A regex is made of small pieces called tokens. A token can be one letter like a, one character class like \d (any digit), or a group in round brackets. On its own, each token matches exactly once.

A quantifier is a mark you put right after a token. It says how many times that token may repeat. For example, a+ means "one or more a characters".

The main idea

A quantifier is a repeat mark. It always belongs to the one token just before it. These are the quantifiers:

  • + means one or more.
  • * means zero or more.
  • ? means zero or one. In other words, the token is optional.
  • {4} means exactly four.
  • {2,} means two or more.
  • {2,5} means from two to five.

Quantifiers are greedy unless you say otherwise: they try the longest match first. Put a ? after one, like +?, and it becomes lazy: it tries the shortest match first.

This lesson is only about quantifiers. Other lessons cover the rest:

Real-life analogyA highlighter with a note on it

Picture a highlighter pen with a small note stuck on it. The note says how much to mark: "one or more words", "zero or more words", or "two to four words". You still choose which word to mark. The note only says how many times in a row. A quantifier is that note, and the token is the word.

In real life: Highlight one word
In JavaScript: A plain token such as a
In real life: Keep highlighting while the same word repeats
In JavaScript: + or * repeating a token
In real life: Highlight as little as you can
In JavaScript: A lazy quantifier such as +?
In real life: Highlight up to the full stop, never past it
In JavaScript: A negated class such as [^"]*, which never goes past a quote

+, *, ?, and {n,m}

REAL OUTPUT

Start with the three short quantifiers:

  • a+ means one or more a characters.
  • a* means zero or more a characters.
  • a? means zero or one a. In other words, the a is optional.

Notice the word "zero". With * and ?, the pattern can still match when the token is not there at all.

A quantifier belongs only to the one token just before it. In colou?r, only the u is optional. So it matches color and colour, but not colouur.

To repeat more than one character, put them in a group. (?:ab) is a group: round brackets with ?: at the start, which only join a and b into one token. So (?:ab)+ repeats the pair ab.

Four small quantifiers and what they findPop out in the code editor (opens in a new tab)JavaScript
const words = "color colour colouur";console.log(words.match(/colou?r/g).join(", "));console.log("aaaab".match(/a+b/)?.[0]);console.log("bbb".match(/a*b/)?.[0]);console.log("ababab".match(/(?:ab)+/)?.[0]);

match() gives back a list of what it found, or null if it found nothing. ?.[0] takes the first item, which is the matched text. If there is no match, ?. gives undefined instead of an error. Each line prints what its pattern found:

  • Line 2: u? allows zero or one u, so it finds color and colour, but not colouur. The g flag at the end means "find every match, not just the first".
  • Line 3: a+ takes the whole run of a characters before b, so it finds aaaab.
  • Line 4: a* may take zero a characters. The text bbb has no a, so it finds just b.
  • Line 5: (?:ab)+ repeats the pair ab, so it finds ababab.
Row 1: the pattern colou?r split into colo, u?, and r. The ? belongs only to u, so u can appear 0 or 1 times. It finds color and colour. Row 2: in (?:ab)+ the + belongs to the whole group ab, which can appear 1 or more times. It finds ababab.colou?r0 or 1 ucolor, colour(?:ab)+1 or more abababab
In colou?r, the ? belongs only to the u. In (?:ab)+, the + belongs to the whole group ab.
+ needs at least one, * allows nonePop out in the code editor (opens in a new tab)JavaScript
console.log(/a+b/.test("b"));console.log(/a*b/.test("b"));console.log("zabab".match(/(?:ab)+/)?.[0]);

test() gives true if the pattern is found anywhere in the text, and false if not.

  • Line 1 prints false. a+ needs at least one a, and the text b has none.
  • Line 2 prints true. a* allows zero a characters, so b alone is enough.
  • Line 3 prints abab. The match does not have to start at the beginning of the text. It skips the z and starts at the first a.

Curly braces give a count:

  • {4} means exactly four.
  • {2,} means two or more.
  • {2,4} means from two to four.

There is no short way to write "up to four". {,4} does not work in JavaScript. To see why, the next code makes a regex from a string with new RegExp("..."):

  • Without the u flag, new RegExp("a{,3}") does not see a quantifier. It looks for the plain text a{,3}.
  • With the u or v flag, the same pattern is an error called a SyntaxError.

To say "from zero to four", write {0,4}.

Counts in curly braces, and why {,3} failsPop out in the code editor (opens in a new tab)JavaScript
const pins = ["123", "1234", "12345", "12a4"];console.log(pins.filter((pin) => /^\d{4}$/.test(pin)).join(", "));console.log("haaa!".match(/ha{2,}/)?.[0]);console.log("aaaaa".match(/a{2,4}/)?.[0]);console.log(new RegExp("a{,3}").test("a{,3}"));try {  new RegExp("a{,3}", "u");} catch (error) {  console.log(error.name);}
  • Line 2 keeps only 1234. \d means one digit, and {4} asks for four of them. The ^ and $ make the check cover the whole string; you will meet them again later in this lesson.
  • Line 3: a{2,} takes two or more a characters, so it finds haaa.
  • Line 4: a{2,4} stops at four, so it finds aaaa out of five.
  • Line 5 prints true. Without u, the pattern looks for the plain text a{,3}, and finds it.
  • Line 7 adds the u flag and throws an error. The catch part runs instead, and line 9 prints SyntaxError.

try and catch work as a pair. JavaScript runs the code in try. If that code throws an error, the code in catch runs instead, with the error in error.

Sort the quantifier
  • /a+/
  • /a*/
  • /colou?r/
  • /\d{4}/
  • /a{2,4}/
  • /.+?/
Try it yourself
0 of 6 correct

Put each pattern in a group by the smallest number of copies it allows. A lazy ? after a quantifier changes which size is tried first, not the smallest number.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Greedy quantifiers take the most first

STEP THROUGH

In JavaScript, quantifiers are greedy unless you say otherwise. Greedy means: take as many characters as you can first. Greedy is not a mistake. It is just the order in which JavaScript tries things.

After the greedy part takes all it can, JavaScript checks the rest of the pattern. If the rest cannot match, the greedy part gives back one character, and JavaScript checks again. This giving back is called backtracking. It goes on until the rest of the pattern matches, or until there is nothing left to give back.

Here is a common surprise. The text is say "one" and "two". and the pattern is /".+"/:

  • The first " in the pattern finds the first quote in the text.
  • A dot, ., means any one character. So .+ means one or more of any character. Greedy .+ takes everything up to the end, even the final period.
  • The last " in the pattern still needs a quote. So .+ gives characters back until that " can match the last quote in the text.

The result is one long match, "one" and "two". It is not just "one".

The pattern /".+"/ on the text say "one" and "two". Step 1: .+ grabs everything after the first quote, up to the end, including the final period. Step 2: it gives the period back, so the match is "one" and "two"./".+"/the textsay "one" and "two".1. grab all"one" and "two".2. give back"one" and "two".given back
Greedy .+ first takes everything up to the end. Then it gives characters back until the closing " of the pattern can match the last quote.
Step through greedy ".+": take all, then give back
Step 0 of 9Ready
Your turn: follow the blue line

Greedy ".+" first takes everything to the end. Then it gives back characters only until its last quote can match.

Running in
  1. script
Next: line 1
const engineMatch = text.match(/".+"/)[0];const firstQuote = text.indexOf('"');let cursor = firstQuote + 1;while (cursor < text.length) cursor += 1;while (text[cursor - 1] !== '"') cursor -= 1;const simulated = text.slice(firstQuote, cursor);console.log(engineMatch);console.log(simulated === engineMatch);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The step-through copies what the regex does with a small loop. Line 5 moves a cursor (a position number) to the end of the text, like the greedy grab. Line 6 moves the cursor back until a quote is just before it, like backtracking. Line 9 prints true, because the loop gets the same answer as the real regex on line 2. The loop only works for this one pattern. It is a model to help you picture the steps.

The step-through code on its ownPop out in the code editor (opens in a new tab)JavaScript
const text = 'say "one" and "two".';const engineMatch = text.match(/".+"/)[0];const firstQuote = text.indexOf('"');let cursor = firstQuote + 1;while (cursor < text.length) cursor += 1;while (text[cursor - 1] !== '"') cursor -= 1;const simulated = text.slice(firstQuote, cursor);console.log(engineMatch);console.log(simulated === engineMatch);
Too much backtracking is slow

A little backtracking is normal. But some patterns put a quantifier inside another quantifier, like (a+)+. Such a pattern can match the same text in very many ways. Trying all of them can freeze a page. An attack that does this on purpose is called ReDoS. The Regex performance lesson covers it. For now, notice when a quantifier has many ways to match the same text.

Some other regex tools have possessive quantifiers. A possessive quantifier keeps everything it took and never gives anything back. Those tools write it with a second +, like a++, a*+, or a?+. JavaScript does not have them.

JavaScript has no possessive quantifiersPop out in the code editor (opens in a new tab)JavaScript
console.log("aaab".match(/a+b/)?.[0]);try {  new RegExp("a++");} catch (error) {  console.log(error.name);}
  • Line 1 uses an ordinary + and prints aaab.
  • Line 3 tries to make a++. JavaScript throws an error.
  • Line 5 catches the error and prints its name, SyntaxError.

In JavaScript, give the pattern a clear place to stop instead. The next section shows one way: a negated class.

Lazy quantifiers take the least first

INTERACTIVE

Put a ? right after a quantifier to make it lazy:

  • +? is a lazy +.
  • *? is a lazy *.
  • ?? is a lazy ?.
  • {2,5}? is a lazy {2,5}.

A lazy quantifier tries the shortest match first. It grows one character at a time, and only when the rest of the pattern cannot match yet.

Lazy does not change the count. +? still needs at least one character. It just tries one character before it tries two.

The pattern /".+?"/ on the same text. It tries the shortest body first: "o. The next character is n, so it grows to "on. The next is e, so it grows to "one. The next is a quote, so it stops. The match is "one"./".+?"/"o"on"one"one"next is ngrownext is egrownext is "stopthe match
Lazy .+? starts with one character. It grows one character at a time and stops as soon as the next character is a quote.
Step through lazy ".+?": start small, then grow
Step 0 of 9Ready
Your turn: follow the blue line

Lazy quantifiers start with the shortest match they may take. They grow only while the rest of the pattern cannot match yet.

Running in
  1. script
Next: line 1
const lazy = text.match(/".+?"/)[0];const negated = text.match(/"[^"]*"/)[0];let end = text.indexOf('"') + 2;while (text[end] !== '"') end += 1;const simulated = text.slice(text.indexOf('"'), end + 1);console.log(lazy);console.log(negated);console.log(simulated === lazy);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

There is a third way to match one quoted word: a negated class. A negated class is a set in square brackets that starts with ^. It means "any one character except these". So [^"] means any character except a quote. The pattern /"[^"]*"/ reads:

  1. a quote,
  2. then zero or more characters that are not quotes,
  3. then a quote.

The middle part can never hold a quote, so the match must stop at the next quote. For simple quoted text, this is often easier to read than /".*?"/.

Lazy still has to finish the patternPop out in the code editor (opens in a new tab)JavaScript
console.log("\"\"".match(/".+?"/)?.[0] ?? "no match");console.log("\"\"".match(/"[^"]*"/)?.[0]);console.log("aaab".match(/a+?b/)?.[0]);

In lines 1 and 2, the text "\"\"" is two quote characters, "". Inside a string, \" puts a quote character.

  • Line 1 prints no match. +? still needs one character between the two quotes, and there is none. The ?? after the regex is not a quantifier. It means "if the left side is null or undefined, use the right side".
  • Line 2 prints "". The negated class uses *, so zero characters between the quotes is fine.
  • Line 3 prints aaab, not ab. The search starts at the first a. Lazy a+? takes one a, but the next character is another a, not b. So it grows until the b can match.
Try greedy, lazy, and negated-class patterns on quoted text
The code for the pattern you pickedPop out in the code editor (opens in a new tab)JavaScript
const text = 'say "one" and "two".';const pattern = /".+"/g;const matches = [...text.matchAll(pattern)].map((match) => match[0]);console.log(matches.join(" | "));
Matches/".+"/g
  1. at position 4 "one" and "two"
Try it yourself
Pattern

Greedy .+ gives one long match when the text has more than one quoted word.

You can change the text, but not the three patterns. The list shows each match and where it starts.

The table compares the three patterns on the same text, say "one" and "two". Each row is one pattern.

Three ways to match quoted text
PatternHow it searchesWhat it finds in the text
Greedy: /".+"/It takes as much as it can first. Then it gives characters back until the closing quote matches.One long match: "one" and "two".
Lazy: /".+?"/It takes one character first. Then it grows one character at a time until the closing quote matches.Only the first quoted word: "one".
Negated class: /"[^"]*"/It takes only characters that are not quotes, so it must stop at the next quote.Only the first quoted word: "one".

Where quantifiers show up in real code

APPLY IT

Quantifiers are small, but you will see them in a lot of everyday code:

  • a four-digit PIN,
  • cleaning up extra spaces,
  • quoted values, like name="Ada",
  • simple Markdown, like **bold**.

They often work together with character classes from the last lesson, and with anchors from a later lesson.

PINs, cleanup, quotes, and simple MarkdownPop out in the code editor (opens in a new tab)JavaScript
const pins = ["4829", "48290", "48a9"];console.log(pins.filter((pin) => /^\d{4}$/.test(pin)).join(", "));console.log("too    much \t space".replace(/\s+/g, " "));const attributes = 'name="Ada" title="Engineer"';console.log(attributes.match(/"[^"]*"/g).join(", "));console.log("Use **bold** here".replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"));
  • Line 2 uses ^\d{4}$ to keep only strings that are exactly four digits. ^ means "the start of the string", and $ means "the end of the string". These marks are called anchors. With them, the check covers the whole string. The Anchors & lookaround lesson covers them.
  • Line 3: \s means one space-like character, such as a space or a tab. \s+ matches a run of them, and replace turns each run into one space.
  • Line 5 finds each quoted value with the negated class "[^"]*".
  • Line 6 turns **bold** into <strong>bold</strong>. \*\* means two real stars; the backslash stops * from being a quantifier. [^*]+ takes one or more characters that are not stars. The round brackets save that text, and $1 puts it back. The next lesson explains saving text with groups.
Optional country code in a phone fieldPop out in the code editor (opens in a new tab)JavaScript
const phones = ["+91 9876543210", "9876543210", "+91 12345"];const phonePattern = /^(?:\+\d{1,3} )?\d{10}$/;console.log(phones.filter((phone) => phonePattern.test(phone)).join(", "));
  • Line 2 has the group (?:\+\d{1,3} ): a plus sign, one to three digits, and a space. \+ is a real plus sign, not a quantifier.
  • The ? right after that group makes the whole group optional. The space is inside the group, so a number without a country code needs no space at the start.
  • Then \d{10} needs exactly ten digits.
  • Line 3 prints +91 9876543210, 9876543210. The short number +91 12345 fails, because 12345 is not ten digits.

This only checks the format of these sample numbers. Real phone numbers around the world need more rules.

Common traps

  • ".* means up to the next thing I see." No. Greedy .* first goes as far as it can. Then it gives back only as much as the rest of the pattern needs.
  • "* means the token must be in the text." No. * can match nothing at all. This matters when you use the g flag with replace and match.
  • "\d{4} checks for a four-digit string." No. It only finds four digits somewhere in the text. Add ^ and $ to check the whole string.
  • "Repeating a capture group saves every repeat." No. A capture group is a group in plain round brackets, like ([abc]). It saves the text it matched. When it repeats, JavaScript keeps only the last repeat.

Each row of the table is one trap: what goes wrong, why, and what to do instead.

Quantifier traps and what to do instead
The trapWhy it happensWhat to do instead
.* in text that looks like HTMLGreedy .* can run from the first opening tag to the last closing tag.Use an HTML parser for real web pages. In a tiny example, use [^<]* or a lazy .*?.
* with replace and the g flag* can match zero characters, so it matches at every spot between characters.Ask yourself if zero characters should really count. If not, use +.
Checking a length without ^ and $/\d{4}/ finds four digits inside 12345, so the test passes.Add ^ and $, like /^\d{4}$/, so the whole string must match.
Repeating a capture groupThe group saves over its text each time it repeats, so only the last repeat is kept.Use (?:...) when you only need to join characters. To get every piece, find each piece on its own.
Greedy .* jumps across two tagsPop out in the code editor (opens in a new tab)JavaScript
const html = "<strong>one</strong><strong>two</strong>";console.log(html.match(/<strong>.*<\/strong>/)[0]);console.log(html.match(/<strong>.*?<\/strong>/)[0]);console.log(html.match(/<strong>[^<]*<\/strong>/)[0]);

The text here looks like HTML, but it is just a string. In <\/strong>, the backslash lets the regex hold a / without ending.

  • Line 2 uses greedy .*, so the match runs from the first <strong> to the last </strong>.
  • Line 3 uses lazy .*?, so it stops at the first </strong>.
  • Line 4 uses [^<]*, which can never go past a <. It also stops at the first </strong>.

For real web pages, use an HTML parser, not a regex.

* can match nothing at allPop out in the code editor (opens in a new tab)JavaScript
console.log("abc".replace(/x*/g, "-"));console.log("xx".replace(/x*/g, "-"));console.log("abc".match(/x*/g).length);

An empty match is a match of zero characters. It happens at a spot between characters.

  • Line 1: x* finds zero x characters at four spots: before a, between the letters, and after c. replace puts a - at each spot, so it prints -a-b-c-.
  • Line 2: in xx, x* first takes both x characters. Then it finds one more empty match at the end. So it prints --.
  • Line 3: match with the g flag finds 4 matches in abc, and all of them are empty.
The code "abc".replace(/x*/g, "-"). The letters a, b, and c, with four arrows at the spots before a, between the letters, and after c. At each spot x* matches zero x characters. The result is -a-b-c-."abc".replace(/x*/g, "-")abc4 empty matchesresult-a-b-c-
x* can match zero x characters, so it finds an empty match at four spots. The replacement puts a - at each spot.

replace and match move on by one character after an empty match, so they always finish. If you write your own loop with exec(), you must do that step yourself. lastIndex is the position where the regex starts its next search. After an empty match, add 1 to lastIndex. Otherwise the loop finds the same empty match again and again.

A repeated capture group keeps only the last repeatPop out in the code editor (opens in a new tab)JavaScript
const capture = "abc".match(/([abc])+/);console.log(capture[0]);console.log(capture[1]);console.log("ababab".match(/(?:ab)+/)[0]);
  • Line 2 prints abc, the full match.
  • Line 3 prints c. The group ([abc]) ran three times: a, then b, then c. Each time, it saved over the last one, so only c is left.
  • Line 4 uses (?:ab)+. This group only joins characters together. It saves nothing.

To get every repeated piece, find each piece on its own. For example, use matchAll with a pattern for one piece and the g flag. matchAll gives back every match, one by one.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the optional u

Read the three tests. Type the three values that console.log prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const pattern = /colou?r/;
console.log(pattern.test("color"), pattern.test("colour"), pattern.test("colouur"));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a counted range

    Type the exact text that the match finds.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const code = "Item 12345";
    console.log(code.match(/\d{2,4}/)[0]);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the greedy quote bug

      Change the pattern so that the match is only "one".

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const text = 'say "one" and "two".';
      console.log(text.match(/".+"/)[0]);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpot the empty matches

        Type the exact string that the replacement prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        console.log("abc".replace(/x*/g, "-"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWrite an exact PIN pattern

          Write a regex pattern for exactly four digits, like a simple PIN. Use the anchors ^ and $. The Anchors lesson explains them in more detail.

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does a quantifier repeat?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the empty-match replacement print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log("abc".replace(/x*/g, "-"));

              Choose an answer to see the explanation.

            3. Question 3 of 8What does the greedy quoted pattern print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const text = 'say "one" and "two".';
              console.log(text.match(/".+"/)[0]);

              Choose an answer to see the explanation.

            4. Question 4 of 8What does the lazy quoted pattern print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const text = 'say "one" and "two".';
              console.log(text.match(/".+?"/)[0]);

              Choose an answer to see the explanation.

            5. Question 5 of 8What does JavaScript do with {,3} here?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(new RegExp("a{,3}").test("a{,3}"));
              try {
                new RegExp("a{,3}", "u");
              } catch (error) {
                console.log(error.name);
              }

              Choose an answer to see the explanation.

            6. Question 6 of 8Why do ^ and $ matter for length checks?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(/\d{4}/.test("12345"));
              console.log(/^\d{4}$/.test("12345"));

              Choose an answer to see the explanation.

            7. Question 7 of 8What does the repeated capture group keep?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const match = "abc".match(/([abc])+/);
              console.log(match[1]);

              Choose an answer to see the explanation.

            8. Question 8 of 8For simple quoted text with no quotes inside, which pattern says most clearly where to stop?

              Choose an answer to see the explanation.

            Key takeaways

            • A quantifier repeats only the one token just before it. To repeat more than one character, put them in a group, like (?:ab)+.
            • + needs at least one. * and ? can match zero.
            • Use {n} for exactly n, {n,} for n or more, and {n,m} for n to m. For "up to m", write {0,m}, not {,m}.
            • Greedy quantifiers try the longest match first, then give characters back. Lazy quantifiers try the shortest match first, then grow.
            • For simple quoted text, a negated class like "[^"]*" is often easier to read than a lazy .*?, because it says exactly where to stop.

            Remember the one-liner.
            A quantifier says how many copies of one token can match. Greedy and lazy say which size JavaScript tries first.

            Up next: Groups & backreferences. There you will learn groups that save text, groups that only join, "this or that" choices, and how to match the same text again.

            CompleteFrontend Clear concepts. Working examples.