skip to content

Groups, Backreferences, and Lookaround

Groups let you extract parts of a match and name them for readable code, while lookahead and lookbehind assert context without consuming characters. Interviewers use these to test whether you can express a real parsing constraint rather than bolt on more .*.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In a JavaScript regular expression, what is the difference between (Mr|Ms) and (?:Mr|Ms), and when would you choose the non-capturing form?

level: juniorimportance: must knowfreq 62%

answer

  1. parentheses do two jobs at once
  2. grouping versus recording the text
  3. group numbers come from opening parens
  4. (?: keeps the result array clean

basics

~20 s

Both parenthesis forms group a sub-pattern so quantifiers and alternation apply to the whole unit. Plain parentheses also capture, adding the matched text to the result array; (?: ... ) groups without capturing, so it never occupies a group number.

solid answer

~40 s

Parentheses in a JavaScript regex do two jobs: they group a sub-pattern into one unit, and they capture the text that unit matched. `(?:` turns off only the capturing half. Capturing groups are numbered by the order of their opening parenthesis, left to right, and their matched text shows up as `m[1]`, `m[2]`, ... on the match object returned by `exec`. Use `(?:` whenever you only need grouping — for an alternation like `(?:Mr|Ms)` or to apply a quantifier to several tokens — because every unnecessary capture adds a slot you must skip over and, worse, renumbers every group after it the moment someone edits the pattern. Named groups are the third option when a captured value is genuinely meaningful.

code

javascript · 7 lines
javascript
const withCapture = /(Mr|Ms)\.\s(\w+)/;
const a = withCapture.exec('Mr. Smith');
console.log(a[1], a[2]); // 'Mr' 'Smith'

const noCapture = /(?:Mr|Ms)\.\s(\w+)/;
const b = noCapture.exec('Mr. Smith');
console.log(b[1], b[2]); // 'Smith' undefined

go deeper

for a junior

Know that plain parentheses both group and capture while (?: ... ) only groups, and be able to point at which element of the match result each group fills.

for a middle

Explain that numbering follows opening parentheses left to right, so inserting one capture renumbers every later group, and that a quantified group retains only its final iteration.

for a senior

Make the maintenance argument out loud: positional captures couple consumer code to pattern edits, and you promote a value to a named group as soon as more than one capture is meaningful.

for a principal

Own the convention for patterns that outlive their author — capture only what a caller consumes, prefer named groups in shared patterns, and know when a regex has grown into something that should be a small parser instead.

## Two jobs behind one piece of syntax A pair of parentheses in a JavaScript regular expression does two independent things. It **groups**: the enclosed sub-pattern becomes a single unit, so a quantifier or an alternation bar applies to all of it rather than to the last character. `/ab+/` repeats only `b`; `/(?:ab)+/` repeats the pair. Alternation is the other classic case — `/^(?:cat|dog)$/` anchors both branches, while `/^cat|dog$/` does not. It **captures**: the text the unit matched is recorded and handed back with the match result. That is the half `(?:` switches off. ## What capturing actually produces `RegExp.prototype.exec` returns an array-like match object. Element `0` is the entire matched substring; elements `1`, `2`, `3`, … are the substrings matched by the capturing groups. Group numbers are assigned by the position of the **opening** parenthesis, counted left to right through the whole pattern, nesting included — so in `/((a)(b))/` group 1 is the outer group, group 2 is `a`, group 3 is `b`. ```js const withCapture = /(Mr|Ms)\.\s(\w+)/; const a = withCapture.exec('Mr. Smith'); console.log(a[1], a[2]); // 'Mr' 'Smith' const noCapture = /(?:Mr|Ms)\.\s(\w+)/; const b = noCapture.exec('Mr. Smith'); console.log(b[1], b[2]); // 'Smith' undefined ``` The second pattern still needs the parentheses — without them `Mr|Ms\.\s(\w+)` would mean "`Mr`, or `Ms` followed by a dot" — but it does not need the captured value, so `(?:` keeps the honorific out of the result and slides the surname down to `m[1]`. ## Why numbering fragility is the real argument The cost of a stray capture is not memory, it is coupling. Consumer code reads positions: `m[1]`, `m[2]`. Add one capturing group near the front of a pattern six months later and every position after it shifts by one, silently. Nothing throws; the code just starts reading the wrong field. Patterns that capture only what a caller consumes are far more resistant to that edit. This is also why named groups exist: `(?<surname>\w+)` gives the value a stable identity that survives renumbering entirely. ## A quantified group keeps only the last iteration A subtlety worth having ready: repeating a capturing group does not accumulate the repetitions. The group's stored capture is overwritten on each iteration, so only the final one survives. ```js console.log(/(\w)+/.exec('abc')[1]); // 'c' — last iteration wins console.log(/(\w+)/.exec('abc')[1]); // 'abc' — quantifier inside the group ``` If you want the whole run, put the quantifier **inside** the group. If you want each item separately, you need repeated matching, not a repeated group. ## Nesting and the empty-match case Groups nest freely, and an inner group is numbered after the outer one that contains it. A group that is present in the pattern but never participates in the successful match — for example the untaken branch of an alternation, or an optional group that matched nothing — yields `undefined` in the result array, not an empty string. That distinction matters when you feed captured values straight into string concatenation. ## Performance, honestly stated Non-capturing groups are sometimes sold as an optimisation. In modern engines the difference is negligible for ordinary patterns; capturing means the engine must save and restore a position pair on backtracking, which is real but tiny. Choose `(?:` for clarity and edit-safety and treat any speed benefit as a rounding error. The genuinely expensive regex problems come from backtracking behaviour, not from how many groups you declared. ## Choosing between the three forms A practical rule: use `(?:` by default when you are only grouping; use a plain capturing group when a caller reads the value positionally and the pattern is small; use a named group `(?<name>…)` as soon as the pattern has more than one or two meaningful captures, or when the pattern is likely to be edited by someone who did not write it. The three forms cost the same to type, and the choice is a readability decision that shows up in every line of code that consumes the match.

  • If a pattern contains no capturing groups at all, what does the match result look like?
    You still get element `0`, the full matched text, plus the usual `index` and `input` properties — there are simply no numbered elements after it. The `groups` property is `undefined` because `groups` is only created when the pattern declares at least one named group.
  • How are groups numbered when they nest, and why does that trip people up?
    Numbering follows the order of the opening parenthesis, scanning left to right, so an outer group always gets a lower number than the groups inside it. In `/((a)(b))/` the outer group is 1, `a` is 2, `b` is 3. People expect completion order or innermost-first, and read the wrong slot.
  • What does /(\d)+/ capture in group 1 when it matches "123", and how do you capture the whole run instead?
    Group 1 holds `'3'`. The group is entered three times and its capture is overwritten each iteration, so only the last one survives. Move the quantifier inside the group — `/(\d+)/` — to capture `'123'` as a single unit.

saying these in an interview costs you the question

  • Thinks (?: changes what text the pattern matches
  • Says non-capturing groups exist mainly for speed
  • Believes group numbers follow closing parentheses or nesting depth
  • Expects a repeated group to collect every iteration
  • Assumes an unmatched group yields an empty string

context

open as a page

Explain lookahead and lookbehind assertions in JavaScript regular expressions — what does "zero-width" mean, and give a case where a lookahead is the only clean solution.

level: middleimportance: must knowfreq 66%

basics

~20 s

Lookaround tests whether a sub-pattern matches at the current position without consuming characters, so the matched text excludes it. (?=x) and (?!x) look forward, (?<=x) and (?<!x) look backward, and each can be positive or negative.

open as a page

Why does /^cat|dog$/.test('hotdog') return true in JavaScript, and how do you write the pattern you probably meant?

level: middleimportance: should knowfreq 42%

basics

~20 s

Alternation has the lowest precedence in a regex, so the pattern splits into two whole branches: ^cat, or dog$. 'hotdog' ends with dog, satisfying the second branch. Group the branches — /^(?:cat|dog)$/ — to make the anchors apply to both.

open as a page

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?

level: middleimportance: should knowfreq 55%

basics

~20 s

Named 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.

open as a page

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%

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.

open as a page