The d flag: capture group start and end offsets
The d flag adds an indices array to every match result, giving the start and end offset of the whole match and of each capture group that participated.
At a glance
- Flag letter
- d
- Property
- RegExp.prototype.hasIndices
- Added in
- ES2022
- Adds
- match.indices, an array of [start, end] pairs parallel to the match array
- Named groups
- match.indices.groups mirrors match.groups, and is undefined if the pattern has none
- Unmatched group
- its slot in indices is undefined, not a zero length range
In detail
A match result has always carried .index for the whole match. What it never carried was the position of each capture group, and that is precisely what syntax highlighting, inline annotation and source mapping need. Before ES2022 you either re-ran a narrower pattern per group or counted characters by hand, and both approaches drift the moment the pattern gains an optional part.
With d set, the result gains an indices array whose slots line up with the match array. Slot zero is the whole match, slot one is capture group one, and so on. The end offset is exclusive, so passing a pair straight into slice returns the group text back, which is the quickest way to check you are reading the array correctly.
Groups that did not participate get undefined rather than a placeholder range. An optional group that failed to match has undefined in both the match array and the indices array, so the same truthiness check works for either. If the pattern has no named groups at all, indices.groups is undefined rather than an empty object.
The flag costs an extra array per match, so leave it off for a plain test or a replace where you only want the resulting string. Turn it on when your own code is going to draw something on top of the original text and needs to know exactly where each piece came from.
Examples
const m = /(?<year>\d{4})-(\d{2})/d.exec("due 2026-03-08");
m[0]; // "2026-03"
m.index; // 4
m.indices[0]; // [4, 11] the whole match
m.indices[1]; // [4, 8] the year group
m.indices[2]; // [9, 11] the month group
m.indices.groups.year; // [4, 8]const opt = /(a)?(b)/d.exec("b");
opt[1]; // undefined
opt.indices[1]; // undefined
opt.indices[2]; // [0, 1]Things that catch people out
- match.indices only exists when the d flag was set. Read it from a match produced without d and you get undefined, and the TypeError surfaces on the next line instead of the one that caused it.
- The end offset is exclusive. Treating it as inclusive is off by one on every single group, and the resulting bug looks like a pattern problem rather than an arithmetic one.
- indices.groups is undefined on a pattern with only numbered groups, so destructuring it without a guard throws on exactly the patterns where you did not need it.
- This tester already shows the index of every match and the text of every numbered and named group without the d flag. Turn d on when your own code needs the offsets, not to see more here.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →(?<name>...): named capture groups
Named groups survive being reordered, read as documentation and give you $<name> in replacements. They are still numbered underneath.
(...) capturing groups: numbering and what comes back
How capture groups are numbered, what a group that did not participate returns, and why quantifying a group throws away everything but the last pass.
The g flag: global matching and the lastIndex trap
What the g flag changes in a JavaScript regex, why a reused global pattern skips matches through lastIndex, and when matchAll is the safer choice.
\d: the digit class, and the digits it refuses
In JavaScript \d is exactly [0-9], even with the u flag. Here is why other engines disagree and what to use when you need Unicode digits.
\D: everything that is not a digit
\D is the exact complement of \d, which makes it a stripping and splitting tool. It also matches newlines, spaces and every letter in every script.
\w: the word character class, underscore included
\w is exactly [A-Za-z0-9_] in JavaScript. That includes the underscore and excludes every accented letter, which is where most \w bugs come from.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links