What do named capture groups (?<name>...) give you in a JavaScript regex, how do you read them off a match, and what does \k<name> do?
answer
- a label instead of a position
- survives someone inserting a group
- read from the match's groups object
- backreference repeats text, not pattern
- null-prototype object, undefined when unnamed
basics
~20 sNamed groups label a capture so it can be read by name from the match object's groups property instead of by a fragile position number. \k<name> is a backreference: it matches the exact text that named group already captured.
solid answer
~40 s`(?<year>\d{4})` is an ordinary capturing group that also carries a name. The match object returned by `exec` gains a `groups` object, so you write `m.groups.year` instead of `m[1]` — the value is still available positionally too, since named groups are numbered like any other. That kills the classic bug where inserting a group renumbers everything after it. `\k<year>` is a backreference: inside the same pattern it matches the *text* that group already captured, not the pattern again — so `/\b(?<w>\w+)\s+\k<w>\b/` finds a doubled word. Named groups and lookbehind both landed in ES2018. Two details worth knowing: `groups` is `undefined` when the pattern declares no named group, and the object has a null prototype, so it inherits nothing.
code
javascript · 4 linesconst re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = re.exec('2026-08-19');
console.log(m.groups.year, m.groups.day); // '2026' '19'
console.log(m[1], Object.getPrototypeOf(m.groups)); // '2026' nullgo deeper
Be able to write a named group and read the value with match.groups.name, and say that the same capture is still available by number.
Explain that a backreference matches the previously captured text rather than the pattern, and that groups is undefined for a pattern with no named groups.
Bring the maintenance angle: named groups decouple consumer code from pattern edits, and you should be able to say when the pattern has outgrown regex entirely.
Set the team convention — named groups for any pattern shared across modules, plus the judgement call on where regex parsing stops and a real parser or a structured log format begins.
## What a name buys you A named capture group is written `(?<name>subpattern)`. It captures exactly like `(subpattern)` does — same numbering, same participation rules — and additionally records the capture under a key. Named groups arrived in ES2018 alongside lookbehind. The payoff is at the consumption site. Positional captures couple every reader of the match to the pattern's internal shape: `m[3]` means nothing to someone reading the call site, and it silently becomes the wrong field the day somebody adds a group earlier in the pattern. A name is stable under that edit and self-documenting. ```js const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const m = re.exec('2026-08-19'); console.log(m.groups.year, m.groups.month); // '2026' '08' console.log(m[1]); // '2026' — still numbered ``` The values are strings, always — a regex has no notion of a number, so `m.groups.year` is `'2026'`, not `2026`. ## The groups object itself Two properties of `match.groups` catch people out. First, it is `undefined` — not an empty object — when the pattern declares no named group at all. So `const { groups } = re.exec(s)` followed by `groups.year` throws for an unnamed pattern, and defensive code should either commit to named groups everywhere or check. Second, the object is created with a **null prototype**. It has no `toString`, no `hasOwnProperty`, nothing from `Object.prototype`. That is deliberate: a group named `constructor` or `toString` must not collide with an inherited member. Practically it means `m.groups.hasOwnProperty('year')` throws, and you use `Object.hasOwn(m.groups, 'year')` or `'year' in m.groups` instead. A group that is present in the pattern but did not participate in the match — an optional named group, or one in the untaken branch of an alternation — is present as a key with the value `undefined`. Destructuring makes the result read very cleanly: ```js const { groups: { proto, host } = {} } = /^(?<proto>\w+):\/\/(?<host>[^/]+)/.exec(url) ?? {}; ``` ## Backreferences: matching the same text again A backreference matches whatever a group already captured, character for character. `\1` refers to group 1 by number; `\k<name>` refers to a named group. The key idea — and the usual misconception — is that a backreference repeats the **matched text**, not the pattern. ```js const doubled = /\b(?<w>\w+)\s+\k<w>\b/; console.log(doubled.test('the the cat')); // true console.log(doubled.test('the that')); // false ``` `\k<w>` here means "the literal word we just captured again", which is why this finds duplicated words and not merely two adjacent words. The same trick matches balanced quoting: `/(['"]).*?\1/` requires the closing quote to be the same character as the opening one, which a character class `['"]` on both ends could not enforce. Backreferences are also case-sensitive independently of nothing — they follow the pattern's flags, so with `i` the reference matches case-insensitively. One legacy quirk: in a pattern that declares no named groups and is not in Unicode mode, `\k` is treated as a literal `k` under the Annex B web-compatibility rules. With the `u` flag, `\k<name>` referring to a non-existent group is a syntax error instead. Relying on the legacy behaviour is never intentional. ## Duplicate names Originally, two groups in one pattern could not share a name — a `SyntaxError`. ES2025 relaxed this: the same name may be reused as long as the occurrences are in **different alternatives** of an alternation, so at most one can participate in any given match. That makes patterns like `/(?<ts>\d{4}-\d{2}-\d{2})|(?<ts>\d{2}\/\d{2}\/\d{4})/` expressible without inventing `ts1` and `ts2`. Two groups on the same path still collide. ## When named groups are the wrong reach Names make a pattern longer, and a pattern that needs six of them is usually telling you it has become a parser. For structured input with nesting or escaping — HTML, arbitrary CSV, a full URL — the honest answer in an interview is that a regex is the wrong tool and a real parser exists. Named groups shine for flat, line-shaped input: log lines, dates, versions, simple identifiers.
- Why does m.groups.hasOwnProperty('year') throw even when the group exists?Because the `groups` object is created with a null prototype, so it inherits nothing from `Object.prototype` — including `hasOwnProperty`. That design prevents a group named `toString` or `constructor` from colliding with inherited members. Use `Object.hasOwn(m.groups, 'year')` or the `in` operator instead.
- What is the difference between \k<q> and repeating the group's pattern a second time?Repeating the pattern matches anything the pattern allows, independently each time; the backreference matches the exact text captured earlier. In `/(['"]).*?\1/` the backreference forces the closing quote to be the same character as the opening one, which writing `['"]` twice would not.
- Can two groups in one pattern share a name?Only since ES2025, and only when they sit in different alternatives of an alternation, so at most one can participate in a given match. Two same-named groups on the same path are still a SyntaxError. Before ES2025 any duplicate name was rejected outright.
saying these in an interview costs you the question
- Thinks a backreference re-runs the group's pattern
- Says named groups replace numbered access rather than adding to it
- Expects match.groups to be an empty object for unnamed patterns
- Calls hasOwnProperty directly on the groups object
- Assumes captured values are numbers when the group matched digits