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?
answer
- two kinds of nothing, only one is a string
- skipped is not the same as empty
- undefined leaks into template strings
- the reference constrains nothing at all
- other engines fail where this one succeeds
basics
~20 sA 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 sThere 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 linesconsole.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
Know that a capture you did not match comes back as undefined, and normalise it before putting the value into a string.
Explain the difference between a group that matched empty and one that never participated, and show how each interacts with a || versus a ?? default.
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.
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