Non-capturing groups
Group patterns for quantifiers and alternation without creating a capture — keeping group numbering clean and avoiding unnecessary overhead.
- Write a non-capturing group using (?:…)
- Choose between capturing and non-capturing groups in a real pattern
- Explain how non-capturing groups affect group numbering
Every (…) group is a capturing group — it saves its match and takes up a slot
in the result array. Sometimes you need grouping only for structure (applying a
quantifier to a sequence, or scoping an alternation) and the captured value is
irrelevant. Non-capturing groups (?:…) do exactly that.
The problem with unnecessary captures
Consider matching a domain name like sub.example.com:
const m = "sub.example.com".match(/([\w-]+\.)+[\w-]+/);
console.log(m[0]); // "sub.example.com" — full match
console.log(m[1]); // "example." — last captured repetitionThe (…)+ repeats but also captures — and you get the value from the last
iteration. If you needed group 2 for something else it is now group 2, shifted
by the unwanted group 1. Non-capturing groups prevent this:
const m = "sub.example.com".match(/(?:[\w-]+\.)+[\w-]+/);
console.log(m[0]); // "sub.example.com"
console.log(m[1]); // undefined — no capturing group at allThe pattern still matches correctly; the group is just not saved.
Syntax
Replace the opening ( with (?::
// Capturing — result[1] gets the value
/(foo|bar)+/.exec("foobar"); // result[1] = "bar" (last iteration)
// Non-capturing — no result[1]
/(?:foo|bar)+/.exec("foobar"); // result has no group slotsNon-capturing groups work everywhere capturing groups work: with quantifiers, with alternation, and nested inside other groups.
How it affects group numbering
Non-capturing groups do not consume a group number. This matters when you mix capturing and non-capturing groups:
// All capturing — 4 groups numbered 1–4
const m1 = "2024-03-15".match(/(\d{4})-(\d{2})-(\d{2})/);
// m1[1] = "2024", m1[2] = "03", m1[3] = "15"
// Mixed — only the year and day are captured
const m2 = "2024-03-15".match(/(\d{4})-(?:\d{2})-(\d{2})/);
// m2[1] = "2024", m2[2] = "15" (skips the month group)Removing the month capture shifts the day from group 3 to group 2. This is a common source of bugs when editing an existing pattern — switching a capturing group to non-capturing renumbers all subsequent groups.
When editing a pattern that other code reads by group number, switching a
capturing group to non-capturing (or vice versa) will silently break every
reference to later group numbers. Named groups ((?<name>…)) are immune to
this because names never change — they are another reason to prefer names in
larger patterns.
Alternation grouping
The most common use of (?:…) is scoping an alternation inside a larger pattern:
// Without grouping — | has lowest precedence, splits the whole pattern
/^cat|dog$/.test("dog"); // true — matches "dog" at end
/^cat|dog$/.test("catdog"); // true — "cat" at start OR "dog" at end
// With non-capturing group — alternation is scoped
/^(?:cat|dog)$/.test("cat"); // true
/^(?:cat|dog)$/.test("dog"); // true
/^(?:cat|dog)$/.test("catdog"); // false — must be just oneThe non-capturing group is the right choice here because you do not need the matched word — you only need to know whether the overall pattern matched.
Semantic choice: when to capture
A useful mental rule:
| I need the matched text later | Use (…) — capturing |
|---|---|
| I only need grouping structure | Use (?:…) — non-capturing |
| I need the text and have multiple groups | Use (?<name>…) — named capturing |
Where to go next
Next: backreferences — referencing a previously captured group's value inside the same pattern to match repeated or mirrored text.