Quantifiers: greedy & lazy
Learn JavaScript regex quantifiers, greedy and lazy matching, empty-match traps, and practical patterns for quoted strings, PINs, and cleanup.
- 01Pick the right repeat markUse
+,*,?,{n},{n,}, and{n,m}, and know which ones can match nothing. - 02Follow 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.
- 03Avoid 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".
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:
- How to make a regex, flags, and methods like
test: Patterns & flags. \d,\w, and square-bracket sets like[a-z]: Character classes & sets.- Groups in round brackets: Groups & backreferences, the next lesson. Here you will only see one simple group,
(?:ab)+.
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 OUTPUTStart with the three short quantifiers:
a+means one or moreacharacters.a*means zero or moreacharacters.a?means zero or onea. In other words, theais 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.
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 oneu, so it findscolorandcolour, but notcolouur. Thegflag at the end means "find every match, not just the first". - Line 3:
a+takes the whole run ofacharacters beforeb, so it findsaaaab. - Line 4:
a*may take zeroacharacters. The textbbbhas noa, so it finds justb. - Line 5:
(?:ab)+repeats the pairab, so it findsababab.
colou?r, the ? belongs only to the u. In (?:ab)+, the + belongs to the whole group ab.+ needs at least one, * allows noneconsole.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 onea, and the textbhas none. - Line 2 prints
true.a*allows zeroacharacters, sobalone is enough. - Line 3 prints
abab. The match does not have to start at the beginning of the text. It skips thezand starts at the firsta.
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
uflag,new RegExp("a{,3}")does not see a quantifier. It looks for the plain texta{,3}. - With the
uorvflag, the same pattern is an error called aSyntaxError.
To say "from zero to four", write {0,4}.
{,3} failsconst 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.\dmeans 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 moreacharacters, so it findshaaa. - Line 4:
a{2,4}stops at four, so it findsaaaaout of five. - Line 5 prints
true. Withoutu, the pattern looks for the plain texta{,3}, and finds it. - Line 7 adds the
uflag and throws an error. Thecatchpart runs instead, and line 9 printsSyntaxError.
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.
/a+//a*//colou?r//\d{4}//a{2,4}//.+?/
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.
Greedy quantifiers take the most first
STEP THROUGHIn 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".
.+ first takes everything up to the end. Then it gives characters back until the closing " of the pattern can match the last quote.Greedy ".+" first takes everything to the end. Then it gives back characters only until its last quote can match.
script
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);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.
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);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.
console.log("aaab".match(/a+b/)?.[0]);try { new RegExp("a++");} catch (error) { console.log(error.name);}- Line 1 uses an ordinary
+and printsaaab. - 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
INTERACTIVEPut 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.
.+? starts with one character. It grows one character at a time and stops as soon as the next character is a quote.Lazy quantifiers start with the shortest match they may take. They grow only while the rest of the pattern cannot match yet.
script
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);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:
- a quote,
- then zero or more characters that are not quotes,
- 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 /".*?"/.
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 isnullorundefined, use the right side". - Line 2 prints
"". The negated class uses*, so zero characters between the quotes is fine. - Line 3 prints
aaab, notab. The search starts at the firsta. Lazya+?takes onea, but the next character is anothera, notb. So it grows until thebcan match.
const text = 'say "one" and "two".';const pattern = /".+"/g;const matches = [...text.matchAll(pattern)].map((match) => match[0]);console.log(matches.join(" | "));- at position 4
"one" and "two"
Greedy .+ gives one long match when the text has more than one quoted word.
The table compares the three patterns on the same text, say "one" and "two". Each row is one pattern.
| Pattern | How it searches | What 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 ITQuantifiers 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.
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:
\smeans one space-like character, such as a space or a tab.\s+matches a run of them, andreplaceturns 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$1puts it back. The next lesson explains saving text with groups.
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 12345fails, because12345is 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 thegflag withreplaceandmatch. - "
\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.
| The trap | Why it happens | What to do instead |
|---|---|---|
.* in text that looks like HTML | Greedy .* 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 group | The 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. |
.* jumps across two tagsconst 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 allconsole.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 zeroxcharacters at four spots: beforea, between the letters, and afterc.replaceputs a-at each spot, so it prints-a-b-c-. - Line 2: in
xx,x*first takes bothxcharacters. Then it finds one more empty match at the end. So it prints--. - Line 3:
matchwith thegflag finds 4 matches inabc, and all of them are empty.
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.
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, thenb, thenc. Each time, it saved over the last one, so onlycis 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 EXERCISESuRead the three tests. Type the three values that console.log prints.
const pattern = /colou?r/;
console.log(pattern.test("color"), pattern.test("colour"), pattern.test("colouur"));color and colour match, but colouur does not. So the output is true true false.
Type the exact text that the match finds.
const code = "Item 12345";
console.log(code.match(/\d{2,4}/)[0]);The text has five digits in a row, but {2,4} can take at most four. Greedy takes all four, so the match is 1234.
Change the pattern so that the match is only "one".
const text = 'say "one" and "two".';
console.log(text.match(/".+"/)[0]);text.match(/".+?"/)[0]
text.match(/"[^"]*"/)[0]Both give the first quoted word, "one", for this text. The negated class is often easier to read, as long as the text has no quotes inside the quotes.
Type the exact string that the replacement prints.
console.log("abc".replace(/x*/g, "-"));The pattern finds an empty match at four spots: before a, between the letters, and after c. A - goes into each spot, so it prints -a-b-c-.
Write a regex pattern for exactly four digits, like a simple PIN. Use the anchors ^ and $. The Anchors lesson explains them in more detail.
/^\d{4}$/\d{4} asks for four digits. ^ and $ make the check cover the whole string, so 48290 fails.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does a quantifier repeat?
Choose an answer to see the explanation.
Question 2 of 8What does the empty-match replacement print?
Read the code, then predictconsole.log("abc".replace(/x*/g, "-"));Choose an answer to see the explanation.
Question 3 of 8What does the greedy quoted pattern print?
Read the code, then predictconst text = 'say "one" and "two".'; console.log(text.match(/".+"/)[0]);Choose an answer to see the explanation.
Question 4 of 8What does the lazy quoted pattern print?
Read the code, then predictconst text = 'say "one" and "two".'; console.log(text.match(/".+?"/)[0]);Choose an answer to see the explanation.
Question 5 of 8What does JavaScript do with
{,3}here?Read the code, then predictconsole.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.
Question 6 of 8Why do
^and$matter for length checks?Read the code, then predictconsole.log(/\d{4}/.test("12345")); console.log(/^\d{4}$/.test("12345"));Choose an answer to see the explanation.
Question 7 of 8What does the repeated capture group keep?
Read the code, then predictconst match = "abc".match(/([abc])+/); console.log(match[1]);Choose an answer to see the explanation.
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.