Code of the Day
IntermediateGroups and references

Non-capturing groups

Group patterns for quantifiers and alternation without creating a capture — keeping group numbering clean and avoiding unnecessary overhead.

Regular ExpressionsIntermediate7 min read
Recommended first
By the end of this lesson you will be able to:
  • 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 repetition

The (…)+ 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 all

The 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 slots

Non-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 one

The 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 laterUse (…) — capturing
I only need grouping structureUse (?:…) — non-capturing
I need the text and have multiple groupsUse (?<name>…) — named capturing
JavaScript — editable, runs in your browser

Where to go next

Next: backreferences — referencing a previously captured group's value inside the same pattern to match repeated or mirrored text.

Finished reading? Mark it complete to track your progress.

On this page