regex-quantifiers
How to Use Quantifiers in JavaScript Regular Expressions
When you write a regular expression, matching a single character is rarely enough. You almost always need to express "one or more digits," "zero or more spaces," or "exactly 6 hex characters." This is where quantifiers come in. They attach to a character, character class, or group and specify how many times that element should repeat.
This guide covers the three forms of curly brace quantifiers ({n}, {n,m}, {n,}), the shorthand symbols (+, *, ?), and the critical difference between greedy and lazy matching behavior, which is one of the most common sources of regex bugs.
Exact Quantifier: {n}
The simplest quantifier is {n}, which matches exactly n occurrences of the preceding element.
// Match exactly 3 digits in a row
const threeDigits = /\d{3}/;
console.log(threeDigits.test("123")); // true
console.log(threeDigits.test("12")); // false (only 2 digits)
console.log(threeDigits.test("1234")); // true (contains 3 digits in a row)
Notice that "1234" matches because it contains a sequence of 3 digits. The pattern does not require the entire string to be exactly 3 digits unless you add anchors:
// Match a string that IS exactly 3 digits
const exactlyThreeDigits = /^\d{3}$/;
console.log(exactlyThreeDigits.test("123")); // true
console.log(exactlyThreeDigits.test("1234")); // false
console.log(exactlyThreeDigits.test("12")); // false
Practical Examples with {n}
// Match a 6-digit hex color code
const hexColor = /^#[0-9a-fA-F]{6}$/;
console.log(hexColor.test("#ff6600")); // true
console.log(hexColor.test("#FFF")); // false (only 3 hex chars)
console.log(hexColor.test("#gggggg")); // false (not hex characters)
// Match a date in YYYY-MM-DD format (structure only, not value validation)
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
console.log(datePattern.test("2024-01-15")); // true
console.log(datePattern.test("24-1-5")); // false
console.log(datePattern.test("2024/01/15")); // false (wrong separator)
// Match a US ZIP code (5 digits)
const zipCode = /^\d{5}$/;
console.log(zipCode.test("90210")); // true
console.log(zipCode.test("9021")); // false
console.log(zipCode.test("902101")); // false
Range Quantifier: {n,m}
The {n,m} quantifier matches at least n and at most m occurrences.
// Match between 2 and 4 digits
const twoToFour = /\d{2,4}/;
console.log(twoToFour.test("1")); // false (only 1 digit)
console.log(twoToFour.test("12")); // true
console.log(twoToFour.test("123")); // true
console.log(twoToFour.test("1234")); // true
console.log(twoToFour.test("12345")); // true (contains 2-4 digit sequence)
How Many Does It Actually Match?
When there are more characters available than the minimum, the quantifier matches as many as possible (up to the maximum). This is the greedy behavior, which we will explore in detail later.
const twoToFour = /\d{2,4}/g;
console.log("1 12 123 1234 12345".match(twoToFour));
// Output: ['12', '123', '1234', '1234']
Let's break this down:
"1"has only 1 digit, so no match"12"has exactly 2 digits, matches"12""123"has 3 digits, matches all 3 as"123""1234"has 4 digits, matches all 4 as"1234""12345"has 5 digits, the quantifier takes the first 4 as"1234", then"5"alone is only 1 digit (below minimum), so no second match from that group
Practical Examples with {n,m}
// US ZIP code: 5 digits, optionally followed by a hyphen and 4 more digits
const zipCodeFull = /^\d{5}(-\d{4})?$/;
console.log(zipCodeFull.test("90210")); // true
console.log(zipCodeFull.test("90210-1234")); // true
console.log(zipCodeFull.test("90210-12")); // false
// Match hex color codes: 3 or 6 hex characters after #
const hexColor = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
console.log(hexColor.test("#fff")); // true
console.log(hexColor.test("#ff6600")); // true
console.log(hexColor.test("#ffff")); // false (4 chars, neither 3 nor 6)
// Username: 3 to 20 alphanumeric characters or underscores
const username = /^[a-zA-Z0-9_]{3,20}$/;
console.log(username.test("jo")); // false (too short)
console.log(username.test("john_doe")); // true
console.log(username.test("a_very_long_username_!!")); // false (invalid chars)
Do not put spaces inside curly braces. {2, 4} with a space is not a valid quantifier and will be treated as a literal string {2, 4} in most cases.
// ❌ Wrong: space inside the quantifier
const bad = /\d{2, 4}/;
console.log(bad.test("123")); // false (treated as literal text)
// ✅ Correct: no space
const good = /\d{2,4}/;
console.log(good.test("123")); // true
Minimum Quantifier: {n,}
The {n,} quantifier matches at least n occurrences with no upper limit.
// Match 2 or more digits
const twoOrMore = /\d{2,}/;
console.log(twoOrMore.test("1")); // false
console.log(twoOrMore.test("12")); // true
console.log(twoOrMore.test("123456")); // true
// Find all sequences of 3 or more consecutive vowels
const vowelCluster = /[aeiou]{3,}/gi;
const text = "beautiful queue Hawaiian";
console.log(text.match(vowelCluster));
// Output: ['eau', 'ueue', 'aiia']
// Match words with 5 or more characters
const longWords = /\b[a-zA-Z]{5,}\b/g;
const sentence = "The quick brown fox jumps over the lazy dog";
console.log(sentence.match(longWords));
// Output: ['quick', 'brown', 'jumps']
Summary of Curly Brace Quantifiers
| Syntax | Meaning | Example |
|---|---|---|
{n} | Exactly n times | \d{4} matches exactly 4 digits |
{n,m} | At least n, at most m times | \d{2,4} matches 2, 3, or 4 digits |
{n,} | At least n times, no maximum | \d{2,} matches 2 or more digits |
Shorthand Quantifiers: +, *, ?
JavaScript provides three shorthand symbols that cover the most common quantifier patterns. They are simply shorter ways to write specific {n,m} forms.
The + Quantifier: One or More
The + matches one or more occurrences. It is equivalent to {1,}.
// Match one or more digits
const digits = /\d+/g;
const str = "I have 3 cats and 12 dogs";
console.log(str.match(digits));
// Output: ['3', '12']
The key property of + is that it requires at least one match. If the element does not appear at all, the pattern fails at that position.
const oneOrMoreLetters = /^[a-zA-Z]+$/;
console.log(oneOrMoreLetters.test("hello")); // true
console.log(oneOrMoreLetters.test("")); // false (zero letters)
console.log(oneOrMoreLetters.test("hello1")); // false (digit present)
// Extract words (sequences of word characters)
const words = /\w+/g;
const text = "Hello, World! How's it going?";
console.log(text.match(words));
// Output: ['Hello', 'World', 'How', 's', 'it', 'going']
The * Quantifier: Zero or More
The * matches zero or more occurrences. It is equivalent to {0,}.
This means the element is entirely optional but can repeat as many times as needed.
// Match 'a' followed by zero or more 'b's
const pattern = /ab*/g;
const str = "a ab abb abbb ac";
console.log(str.match(pattern));
// Output: ['a', 'ab', 'abb', 'abbb', 'a']
Notice that "a" alone matches because b* allows zero b's. The "a" in "ac" also matches for the same reason.
// Match an optional sign followed by digits
const signedNumber = /[+-]?\d+/g;
const str = "Temp: +5, -3, 42, and +100";
console.log(str.match(signedNumber));
// Output: ['+5', '-3', '42', '+100']
// Match lines that may or may not have leading spaces
const leadingSpaces = /^ *\S+/gm;
const text = "hello\n world\n indented\nflush";
console.log(text.match(leadingSpaces));
// Output: ['hello', ' world', ' indented', 'flush']
* Trap: Empty MatchesBecause * allows zero occurrences, it can produce empty string matches in unexpected places:
const pattern = /\d*/g;
const str = "abc";
console.log(str.match(pattern));
// Output: ['', '', '', '']
This happens because \d* successfully matches "zero digits" at every position in the string (before a, before b, before c, and at the end). This is almost never what you want. If you need at least one digit, use + instead.
The ? Quantifier: Zero or One
The ? matches zero or one occurrence. It is equivalent to {0,1}. It makes the preceding element optional.
// Match "color" or "colour" (the 'u' is optional)
const spelling = /colou?r/g;
console.log("color".match(spelling)); // ['color']
console.log("colour".match(spelling)); // ['colour']
// Match HTTP or HTTPS
const protocol = /https?:\/\//;
console.log(protocol.test("http://example.com")); // true
console.log(protocol.test("https://example.com")); // true
console.log(protocol.test("ftp://example.com")); // false
// Match a number that may or may not have a decimal part
const number = /\d+\.?\d*/g;
const str = "Price: 42 or 42.99 or 0.5";
console.log(str.match(number));
// Output: ['42', '42.99', '0.5']
// Match singular or plural
const apples = /apples?/g;
const text = "1 apple and 5 apples";
console.log(text.match(apples));
// Output: ['apple', 'apples']
Shorthand Quantifiers Summary
| Symbol | Equivalent | Meaning | Description |
|---|---|---|---|
+ | {1,} | One or more | Must appear at least once |
* | {0,} | Zero or more | Fully optional, can repeat |
? | {0,1} | Zero or one | Optional, appears at most once |
Applying Quantifiers to Groups and Sets
Quantifiers apply to the single element immediately before them. That element can be a literal character, a character class, an escape sequence, or a group.
Quantifier on a Single Character
// 'a{3}' matches exactly three 'a' characters
const pattern = /a{3}/;
console.log(pattern.test("aaa")); // true
console.log(pattern.test("aa")); // false
console.log(pattern.test("aaaa")); // true (contains "aaa")
Quantifier on a Character Set
// '[aeiou]{2}' matches exactly 2 vowels in a row (any combination)
const twoVowels = /[aeiou]{2}/g;
const str = "beautiful painting";
console.log(str.match(twoVowels));
// Output: ['ea', 'au', 'ai']
Quantifier on a Group
// '(ha){3}' matches the sequence "ha" repeated 3 times
const laugh = /(ha){3}/;
console.log(laugh.test("hahaha")); // true
console.log(laugh.test("haha")); // false
console.log(laugh.test("ha ha ha")); // false (spaces break the sequence)
Compare this with ha{3}, which only quantifies the a:
// 'ha{3}' matches 'h' followed by exactly 3 'a's
const hTripleA = /ha{3}/;
console.log(hTripleA.test("haaa")); // true
console.log(hTripleA.test("hahaha")); // false
Always remember: a quantifier applies to the single element directly before it. Use parentheses () to quantify an entire sequence.
ab+meansafollowed by one or moreb(ab)+means the sequenceabrepeated one or more times
console.log("abbb".match(/ab+/)); // ['abbb'] (one 'a', many 'b's)
console.log("ababab".match(/(ab)+/)); // ['ababab'] ('ab' repeated)
Greedy vs. Lazy Quantifiers
This is arguably the most important concept to understand about quantifiers. By default, all quantifiers are greedy. They try to match as many characters as possible. You can make them lazy by adding a ? after the quantifier, which makes them match as few characters as possible.
Greedy Behavior (Default)
A greedy quantifier expands as far as it can while still allowing the overall pattern to succeed.
const greedy = /".+"/g;
const str = 'She said "hello" and "goodbye"';
console.log(str.match(greedy));
// Output: ['"hello" and "goodbye"']
Here is what happened step by step:
- The regex engine finds the first
"at position 9 .+(greedy) tries to match as many characters as possible, consuming everything to the end of the string- The engine then needs to match the closing
", but it is at the end of the string with no"left - It backtracks one character at a time until it finds a
"to satisfy the pattern - The last
"in the string is at position 30 (after "goodbye") - The engine matches the entire span from the first
"to the last"
The result is one big match: "hello" and "goodbye", which probably is not what you intended.
Lazy Behavior (Adding ?)
Adding ? after any quantifier makes it lazy (also called "reluctant" or "non-greedy"). It tries to match as few characters as possible.
| Greedy | Lazy | Meaning |
|---|---|---|
+ | +? | One or more (prefer fewer) |
* | *? | Zero or more (prefer fewer) |
? | ?? | Zero or one (prefer zero) |
{n,m} | {n,m}? | n to m (prefer n) |
{n,} | {n,}? | n or more (prefer n) |
const lazy = /".+?"/g;
const str = 'She said "hello" and "goodbye"';
console.log(str.match(lazy));
// Output: ['"hello"', '"goodbye"']
Now the engine behaves differently:
- Finds the first
"at position 9 .+?(lazy) tries to match as few characters as possible, so it starts with just one character:h- The engine checks if the next character is a closing
". It is not (it ise), so.+?expands by one more - This repeats:
he,hel,hell,hello - After matching
hello, the next character is", so the pattern succeeds - First match:
"hello" - The engine continues and finds another
"at position 21, repeating the process for"goodbye"
Side-by-Side Comparison
const html = "<p>First</p><p>Second</p>";
// Greedy: matches from the first < to the LAST >
const greedy = /<.+>/g;
console.log(html.match(greedy));
// Output: ['<p>First</p><p>Second</p>']
// Lazy: matches from each < to the NEAREST >
const lazy = /<.+?>/g;
console.log(html.match(lazy));
// Output: ['<p>', '</p>', '<p>', '</p>']
const text = "aaa bbb ccc";
// Greedy: a+ matches as many 'a's as possible
console.log(text.match(/a+/)); // ['aaa']
// Lazy: a+? matches as few 'a's as possible (but at least 1)
console.log(text.match(/a+?/)); // ['a']
// Greedy with g flag
console.log(text.match(/a+/g)); // ['aaa']
// Lazy with g flag
console.log(text.match(/a+?/g)); // ['a', 'a', 'a']
The *? Lazy Quantifier
// Greedy *: takes everything between first and last delimiter
const greedy = /\[.*\]/g;
const str = "[first] and [second]";
console.log(str.match(greedy));
// Output: ['[first] and [second]']
// Lazy *?: takes the minimum between each pair
const lazy = /\[.*?\]/g;
console.log(str.match(lazy));
// Output: ['[first]', '[second]']
The ?? Lazy Quantifier
The ?? is the lazy version of ?. While ? prefers to match one character if possible, ?? prefers to match zero characters if possible.
const greedy = /colou?r/;
const lazy = /colou??r/;
// Both match "color" (the 'u' is optional either way)
console.log(greedy.test("color")); // true
console.log(lazy.test("color")); // true
// Both match "colour" (both can include the 'u')
console.log(greedy.test("colour")); // true
console.log(lazy.test("colour")); // true
In most cases ? and ?? produce the same match result, because the overall pattern forces the same outcome. The difference becomes relevant in capture groups or when the lazy preference changes which path the engine explores first.
// Where ?? makes a visible difference
const greedy = /(\d?)_/;
const lazy = /(\d??)_/;
const str = "5_";
console.log(str.match(greedy)[1]); // '5' (greedy ? prefers to match the digit)
console.log(str.match(lazy)[1]); // '' (lazy ?? prefers to skip the digit)
// But the overall match is still '5_' in both cases, because _ must follow
The {n,m}? Lazy Quantifier
const greedy = /\d{2,4}/g;
const lazy = /\d{2,4}?/g;
const str = "123456789";
console.log(str.match(greedy));
// Output: ['1234', '5678'] (greedy takes 4 digits each time)
console.log(str.match(lazy));
// Output: ['12', '34', '56', '78'] (lazy takes 2 digits each time)
An Alternative to Lazy Quantifiers: Negated Sets
In many real-world scenarios, you can avoid the greedy/lazy dilemma entirely by using a negated character set instead of the generic . with a lazy quantifier.
// Task: match quoted strings
// Approach 1: lazy quantifier
const lazy = /".*?"/g;
// Approach 2: negated set (match anything that's NOT a quote)
const negated = /"[^"]*"/g;
const str = 'She said "hello" and "goodbye"';
console.log(str.match(lazy));
// Output: ['"hello"', '"goodbye"']
console.log(str.match(negated));
// Output: ['"hello"', '"goodbye"']
Both approaches produce the same result, but the negated set approach is generally faster because the regex engine does not need to backtrack. It simply matches all non-quote characters until it hits a quote.
// Match content between square brackets
const withLazy = /\[.*?\]/g;
const withNegated = /\[[^\]]*\]/g;
const str = "[first] and [second]";
console.log(str.match(withLazy)); // ['[first]', '[second]']
console.log(str.match(withNegated)); // ['[first]', '[second]']
// Match HTML tags
const withLazy = /<.+?>/g;
const withNegated = /<[^>]+>/g;
const html = "<p>Hello</p><br/>";
console.log(html.match(withLazy)); // ['<p>', '</p>', '<br/>']
console.log(html.match(withNegated)); // ['<p>', '</p>', '<br/>']
When possible, prefer negated character sets over lazy quantifiers. "[^"]*" is typically faster than ".*?" because the engine never needs to backtrack. It greedily consumes all non-quote characters and stops at the first quote.
The negated set approach also makes your intent clearer: "match anything that is not a closing delimiter."
Real-World Examples
Extracting Numbers (Integer and Decimal)
const numbers = /\d+\.?\d*/g;
const text = "Total: 42 items at $3.99 each, tax 0.08";
console.log(text.match(numbers));
// Output: ['42', '3.99', '0.08']
Validating Phone Number Formats
// Match formats: (555) 123-4567, 555-123-4567, 5551234567
const phone = /^(\(\d{3}\)\s?|\d{3}[-.]?)\d{3}[-.]?\d{4}$/;
console.log(phone.test("(555) 123-4567")); // true
console.log(phone.test("555-123-4567")); // true
console.log(phone.test("5551234567")); // true
console.log(phone.test("55-123-4567")); // false
Matching IP Addresses (Structure, Not Value)
// Structural match: four groups of 1-3 digits separated by dots
const ipStructure = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
console.log(ipStructure.test("192.168.1.1")); // true
console.log(ipStructure.test("999.999.999.999")); // true (structurally valid)
console.log(ipStructure.test("1.2.3")); // false (only 3 groups)
console.log(ipStructure.test("1.2.3.4.5")); // false (5 groups)
Parsing Key-Value Pairs
const kvPattern = /(\w+)\s*=\s*"([^"]*)"/g;
const config = 'name = "John" age = "30" city = "NYC"';
let match;
const result = {};
while ((match = kvPattern.exec(config)) !== null) {
result[match[1]] = match[2];
}
console.log(result);
// Output: { name: 'John', age: '30', city: 'NYC' }
Trimming Extra Whitespace
// Replace multiple spaces with a single space
const trimmed = "Hello World !".replace(/\s{2,}/g, " ");
console.log(trimmed);
// Output: "Hello World !"
Matching Repeated Words
// Find words that appear twice in a row
const repeatedWord = /\b(\w+)\s+\1\b/gi;
const text = "This is is a test test sentence with the the repeats";
console.log(text.match(repeatedWord));
// Output: ['is is', 'test test', 'the the']
Common Mistakes and Pitfalls
Mistake 1: Using * When You Need +
// ❌ Wrong: \d* allows zero digits, matching empty strings
const pattern = /\d*/g;
const str = "abc";
console.log(str.match(pattern));
// Output: ['', '', '', ''] (matches empty string at every position!)
// ✅ Correct: \d+ requires at least one digit
const fixed = /\d+/g;
console.log(str.match(fixed));
// Output: null (no digits found, which is the expected behavior)
Mistake 2: Forgetting the Greedy Nature of + and *
// ❌ Unexpected: greedy .+ swallows too much
const greedy = /<.+>/;
const html = "<b>bold</b>";
console.log(html.match(greedy)[0]);
// Output: "<b>bold</b>" (matched from first < to LAST >)
// ✅ Fix with lazy quantifier
const lazy = /<.+?>/;
console.log(html.match(lazy)[0]);
// Output: "<b>"
// ✅ Or better: use negated set
const negated = /<[^>]+>/;
console.log(html.match(negated)[0]);
// Output: "<b>"
Mistake 3: Spaces Inside Curly Braces
// ❌ Wrong: spaces break the quantifier syntax
const bad = /\d{2, 4}/;
console.log(bad.test("123"));
// Output: false (treated as literal text "{2, 4}", not a quantifier)
// ✅ Correct: no spaces
const good = /\d{2,4}/;
console.log(good.test("123"));
// Output: true
Mistake 4: Quantifier Applies to One Element, Not the Whole Preceding Pattern
// ❌ Misunderstanding: "abc+" does NOT match "abcabcabc"
const wrong = /abc+/;
console.log(wrong.test("abcabcabc")); // true, but it matches "abc" (a, b, one+ c)
console.log("abccc".match(/abc+/)[0]); // "abccc" (only 'c' is quantified)
// ✅ To repeat "abc", use a group
const right = /(abc)+/;
console.log("abcabcabc".match(right)[0]); // "abcabcabc"
Mistake 5: Confusing ? as Quantifier vs. ? as Lazy Modifier
The ? symbol has two different roles in regex depending on context:
- Quantifier: After a character/group, it means "zero or one" occurrence
- Lazy modifier: After another quantifier (
+?,*?,{n,m}?), it switches to lazy mode
// ? as a quantifier: makes 's' optional
const plural = /items?/;
console.log(plural.test("item")); // true
console.log(plural.test("items")); // true
// ? as lazy modifier: makes + non-greedy
const lazyPlus = /\d+?/;
console.log("12345".match(lazyPlus)[0]); // '1' (matches minimum (one digit))
These are never ambiguous to the regex engine because a lazy ? always follows another quantifier, while a quantifier ? follows a character or group.
Quick Reference Table
| Quantifier | Greedy | Lazy | Meaning |
|---|---|---|---|
{n} | {n} | {n}? | Exactly n (lazy has no practical effect) |
{n,m} | {n,m} | {n,m}? | Between n and m |
{n,} | {n,} | {n,}? | At least n |
+ | + | +? | One or more |
* | * | *? | Zero or more |
? | ? | ?? | Zero or one |
Greedy (default): matches as many characters as possible, then backtracks if needed.
Lazy (add ?): matches as few characters as possible, then expands if needed.
Negated sets ([^...]) are often a better alternative to lazy quantifiers for delimiter-based matching, offering better performance and clearer intent.
Understanding quantifiers is essential for writing effective regular expressions. Start with the shorthand forms (+, *, ?) for common patterns, use curly braces when you need exact control over repetition counts, and always be mindful of greedy vs. lazy behavior to avoid matching more or less than you intend.