Quantifiers
Quantifiers control how many times the previous element matches. ? = 0-1, * = 0+, + = 1+, {n,m} = bounded. Add ? after to make them lazy / non-greedy.
Greedy vs lazy, possessive, backtracking
EXAMPLE
// 1) Basics
/a?/ // 0 or 1 'a'
/a*/ // 0+ 'a'
/a+/ // 1+ 'a'
/a{3}/ // exactly 3
/a{3,}/ // 3 or more
/a{3,5}/ // 3 to 5
// 2) Greedy by default — match as much as possible
'<a>b</a>'.match(/<.+>/); // ['<a>b</a>'] — greedy: spans both tags
// 3) Lazy — match as little as possible (add ? after the quantifier)
'<a>b</a>'.match(/<.+?>/); // ['<a>'] — stops at first '>'
// 4) Possessive — match as much as possible, no giving back (PCRE / .NET; not in JS by default)
/^a++b$/ // PCRE: possessive +; in JS use atomic group (?>...) or rewrite to avoid backtracking
// 5) The catastrophic backtracking trap
// /(a+)+b/ tested against 'aaaaaaaaaaaaaaaa!' — exponential blowup
// The engine tries every split of the 'a's between the inner + and outer + before failing.
// Avoid: don't nest quantifiers on overlapping sets.
// FIX — atomic group or unique sub-patterns
// PCRE / .NET / Python: (?>a+)+b
// JS: /a+b/ (rewrite — the nested + was redundant)
// 6) Quantifier on a group
/(ab){3}/.test('ababab') // true — 'ab' three times
/(ab)+c/.test('ababc') // true
// 7) Real recipes
// 7a) Match a number (integer, possibly signed)
/^-?\d+$/.test('42') // true
/^-?\d+$/.test('-42') // true
/^-?\d+$/.test('+42') // false (no + prefix)
/^[+-]?\d+$/.test('+42') // true
// 7b) Decimal
/^-?\d+(\.\d+)?$/.test('3.14')
/^-?\d+(\.\d+)?$/.test('3.') // false
/^-?\d+(?:\.\d+)?$/ // non-capturing group via (?:...)
// 7c) Phone (loose)
/^\+?[\d\s()-]{7,20}$/.test('+61 2 1234 5678')
// 7d) URL-ish (do not use for validation — use new URL())
/^https?:\/\/[^\s"<>]{1,2000}$/
// 7e) HTML tag (do not use for parsing — use a parser)
/<\/?[a-z][^>]*>/gi
// 7f) Repeated whitespace collapse
' hello world '.replace(/\s+/g, ' ').trim();
// 8) Quantifiers + character classes
/^[\w-]+$/ // letters, digits, _, -
/^[A-Za-z]{2,4}$/ // 2-4 letters
/^[01]+$/ // binary
// 9) Avoid \d at scale — Unicode
// \d matches [0-9] in most engines, but in some modes it includes Arabic-Indic, etc.
// For ASCII-only digits, use [0-9].
// 10) Quantifier-anchored patterns
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/ // strong password — see lookaround lesson
// 11) Performance + safety
// • Use atomic groups / possessive when supported to neutralise backtracking
// • Anchors (^ $ \b) prune the search space — much faster
// • Replace .* with [^X]* when you know what can't appear
// • In adversarial input, regex DoS (ReDoS) is real — test with long worst-case strings
// • Use RE2 (Go, ripgrep) when you can — linear-time guarantee
// 12) Test patterns at regex101.com — visualise quantifier behaviour + spot lazy/greedy traps
// 13) Cheat sheet
// ? - 0 or 1 (lazy: ??)
// * - 0 or more (lazy: *?)
// + - 1 or more (lazy: +?)
// {n} - exactly n (no lazy variant — already exact)
// {n,} - n or more (lazy: {n,}?)
// {n,m} - between n and m (lazy: {n,m}?)
// Possessive (PCRE):
// ?+ *+ ++ {n,m}+ - no backtracking inside this quantifier
Why it matters
Default quantifiers are greedy — the most common bug is /<.+>/ swallowing past the first >. Add ? to go lazy, or use a negated class ([^>]+) for clarity and speed.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
x? // 0 or 1
x* // 0 or more
x+ // 1 or more
x{3} // exactly 3
x{2,5} // 2 to 5
x*? // lazy (smallest match)
x*+ // possessive (PCRE/Java — no backtracking)
Try it Yourself »
Exercise
Quantifier for "one or more".
/a
/
One character.
Discussion
Loading…