skip to content

In JavaScript, what does exec return for an optional capture group that did not participate in the match, and what does a backreference to such a group match?

level: seniorimportance: nice to knowfreq 28%

answer

  1. two kinds of nothing, only one is a string
  2. skipped is not the same as empty
  3. undefined leaks into template strings
  4. the reference constrains nothing at all
  5. other engines fail where this one succeeds

basics

~20 s

A group that did not participate yields undefined in the match result, not an empty string. A backreference to it succeeds by matching the empty string, so the reference silently imposes no constraint at all.

solid answer

~40 s

There are two distinct outcomes people conflate: a group that matched zero characters, which gives `''`, and a group that never participated — the untaken branch of an alternation, or an optional group that was skipped — which gives `undefined`. That distinction matters because `undefined` flows straight into string concatenation as `"undefined"` and defeats a `||` default differently from `''`. The second half is the sharper trap: per the ECMAScript specification, a backreference to a group whose capture is `undefined` matches the **empty string** and therefore always succeeds. So `/(?:(a)|b)\1/.test('b')` is `true` — `\1` constrains nothing. Engines in some other languages fail that backreference instead, so a pattern ported from elsewhere can quietly become permissive in JavaScript.

code

javascript · 5 lines
javascript
console.log(/(a)?(b)/.exec('b')[1]); // undefined — group skipped
console.log(/(a*)(b)/.exec('b')[1]); // ''        — group matched empty

const m = /^(?:(\w+):)?(\d+)$/.exec('42');
console.log(`[${m[1]}] ${m[2]}`);    // '[undefined] 42'

go deeper

for a junior

Know that a capture you did not match comes back as undefined, and normalise it before putting the value into a string.

for a middle

Explain the difference between a group that matched empty and one that never participated, and show how each interacts with a || versus a ?? default.

for a senior

Demonstrate the production instinct: this class of bug makes a pattern accept too much, so you argue for negative test cases and you flag patterns ported from another regex flavour for exactly this reason.

for a principal

Own the standard — shared validation patterns need negative fixtures and a review rule against backreferences to conditionally-filled groups, because a silently permissive matcher is a security-adjacent defect, not a style issue.

## Two different kinds of "nothing" A capturing group can end a match in three states, and only two of them are commonly recognised. 1. It matched some text — the result holds that string. 2. It matched **zero characters** — the result holds `''`. `/(a*)/.exec('b')[1]` is `''`: the group participated and `a*` legitimately matched empty. 3. It **never participated** — the result holds `undefined`. The group was on a branch the engine did not take, or it was optional and skipped. ```js console.log(/(a)?(b)/.exec('b')); // [ 'b', undefined, 'b', ... ] console.log(/(a*)(b)/.exec('b')[1]); // '' ``` With named groups the same rule applies: the key exists on `match.groups` with the value `undefined`. ## Why the distinction bites in real code Code that reads captures usually assumes strings. `undefined` breaks that assumption in ways `''` does not: ```js const m = /^(?:(\w+):)?(\d+)$/.exec('42'); const label = m[1]; // undefined console.log(`[${label}] ${m[2]}`); // '[undefined] 42' ``` Both `''` and `undefined` are falsy, so `m[1] || 'default'` behaves the same for either — but `m[1] ?? 'default'` does not, and neither does anything that calls a string method on the value or feeds it to `JSON.stringify` inside a larger object. The habit worth forming: normalise optional captures at the boundary, `const label = m[1] ?? '';`, rather than trusting the shape downstream. ## The backreference rule Here is the part interviewers actually probe. A backreference (`\1`, or `\k<name>`) matches the text a group captured. What happens when the group has no capture? The ECMAScript specification is explicit: if the referenced capture is `undefined`, the backreference **matches the empty string and succeeds**. It is not an error, and it is not a failure. ```js console.log(/(?:(a)|b)\1/.test('b')); // true console.log(/(a)?\1b/.test('b')); // true ``` In the first, the `b` branch is taken, group 1 never participates, and `\1` matches nothing — so the whole pattern matches the single character `b`. A reader who intended "whatever the first alternative captured must appear again" has written a constraint the engine ignores. This is genuinely a portability issue. Several other regex flavours treat a backreference to a non-participating group as a **failure**, so the identical pattern rejects the input elsewhere. A pattern copied from documentation or from another codebase can therefore become silently permissive in JavaScript — the worst kind of change, because the tests that would catch it are the negative ones nobody wrote. ## Optional groups and quantified groups interact A related subtlety: making a group optional with `?` is not the same as putting `?` inside it. ```js /(\d+)?x/.exec('x')[1]; // undefined — group skipped /(\d*)x/.exec('x')[1]; // '' — group matched empty ``` And for a group inside a repetition, the capture holds the **last** iteration that participated, so a group that participated on an earlier iteration and not on the final one keeps its earlier value in some patterns — reasoning about that quickly stops being worth it. If you find yourself needing to know, the pattern is too clever and should be split. ## How to write patterns that avoid the trap Three practices cover it. First, prefer named groups for anything optional, so the consuming code reads `m.groups.scheme ?? ''` and the intent is visible. Second, do not lean on a backreference to a group that might not participate. If the pairing is genuinely conditional — an opening delimiter that may be absent — express both cases as separate alternatives, each self-contained, rather than one alternative with an optional half. Third, write a negative test. The whole class of bug here is a pattern that matches **more** than intended, and only an assertion that a bad input is rejected can catch it. A test suite that checks only the happy path will pass on a pattern whose backreference does nothing at all.

  • How do /(a*)/ and /(a)?/ differ when the input has no 'a'?
    `/(a*)/ ` participates and matches zero characters, so the capture is `''`. `/(a)?/` skips the group entirely, so the capture is `undefined`. Both are falsy, so a `||` default hides the difference, but `??`, string methods and JSON serialisation all treat them differently.
  • Why is this backreference behaviour a portability hazard?
    Several other regex flavours make a backreference to a non-participating group *fail*, which is the intuitive reading. JavaScript makes it match the empty string and succeed. A pattern ported into JavaScript therefore accepts inputs it rejected before — a silent loosening that only a negative test case catches.
  • How would you express "an optional opening delimiter must be matched by the same closing one" without relying on that rule?
    Split it into two self-contained alternatives: one that requires both delimiters and uses the backreference, and one that requires neither. Each branch is then internally consistent, and no branch depends on a backreference to a group it did not fill.

saying these in an interview costs you the question

  • Says a skipped group captures an empty string
  • Expects a backreference to an unset group to fail the match
  • Feeds optional captures into templates without normalising
  • Assumes regex backreference semantics are identical across languages
  • Treats undefined and '' as interchangeable because both are falsy

context