Course outline · 0% complete

0/27 lessons0%

Course overview →

Groups, match, and replace

lesson 9-2 · ~11 min · 24/27

Capturing the pieces you care about

Parentheses create a capture group, so the part of the match inside them is saved and numbered from 1, left to right.

Four tools cover the everyday use.

  • string.match(regex) returns the full match at index 0, then each group.
  • string.replace(regex, replacement) can use $1, $2, and so on in the replacement to reinsert groups.
  • Named groups read better, since (?<year>\d{4}) is written back as $<year> and appears in match(...).groups.
  • The g flag makes match and replace handle every occurrence instead of just the first.

Named groups are worth adopting early. A replacement of "$<day>/$<month>/$<year>" survives someone adding a group in the middle, and "$3/$2/$1" does not.

Groups are the regex you actually use at work, including pulling fields out of logs and filenames, reshaping dates, and mass-renaming in an editor.

test only says whether text matches, and groups let you keep the pieces.

When you want grouping without capturing, (?:...) groups for alternation or quantifiers without consuming a number.

pattern(\d{4})-(\d{2})-(\d{2})group 1group 2group 3input2026-07-05replacement "$3/$2/$1"05/07/2026
Each parenthesized group is numbered left to right, and the replacement string reinserts them in any order.

Extracting and reassembling a date

Three groups, then a reordered replacement.

const iso = "2026-07-05";
const parts = iso.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(parts[1]);
console.log(parts[3]);

console.log(iso.replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1"));

console.log("tomato, tomato".replace(/tomato/g, "potato"));

Output

2026
05
05/07/2026
potato, potato

The groups capture the date parts, replace reassembles them in a new order, and the g-flag example rewrites every match in one call.

parts[0] would be the whole "2026-07-05", so the group numbering starts at 1 rather than 0.

match returns null when nothing matches, which is the failure mode to guard. Reading parts[1] off a null throws, so real code checks the result or uses ?. from lesson 8-2.

The replacement string is not code, and $3 has meaning only inside it. A literal dollar sign is written $$.

The named version reads better for anything you will revisit, and /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/ with "$<d>/$<m>/$<y>" produces the identical output.

It returns "a+b-c".

No g flag means replace touches only the first match, so the second hyphen is left alone.

Forgetting g is probably the most common regex bug in real code reviews, and the reason is that the code looks correct and works on single-match inputs.

The symptom is partial success rather than an error. A sanitizer that strips the first bad character passes its simplest test and fails on real data.

replaceAll exists for the plain-string case and removes the ambiguity, so "a-b-c".replaceAll("-", "+") gives "a+b+c".

With a regex, replaceAll requires the g flag and throws a TypeError without it, which is the language turning a silent bug into a loud one.

The practical regex kit

You now have the kit that covers almost all real work.

TaskTool
validatetest, anchored with ^ and $
extract onematch with groups
extract allmatchAll with the g flag
reshapereplace with $1 or $<name>
replace allg flag, or replaceAll

for (const m of str.matchAll(regex)) iterates every match with its groups, and that is the iteration protocol from lesson 7-1 showing up in the standard library.

Flags are worth knowing as a short list too. g for global, i for case-insensitive, m so ^ and $ match line boundaries, and s so . matches newlines.

The interview advice is to keep patterns small and readable, and to narrate them piece by piece. Nobody expects regex golf, and a reviewer would rather read three simple patterns than one clever one.

One performance caveat deserves a mention. Nested quantifiers like (a+)+b can take exponential time on a failing input, which is the denial-of-service pattern called catastrophic backtracking, so keep patterns shallow when they run on user input.

Parsing log lines with matchAll

Every match, with its groups.

const log = "GET /home 200, POST /login 401, GET /admin 403";

for (const m of log.matchAll(/(GET|POST) (\S+) (\d{3})/g)) {
  console.log(m[3] + " " + m[2]);
}

Output

200 /home
401 /login
403 /admin

Each m works like the match array you already know, so m[0] is the whole match and m[1] onward are the groups.

\S+ matches a run of non-whitespace, which is how the path is captured without knowing its length.

The g flag is mandatory here, and matchAll throws a TypeError without it rather than silently returning one match.

Compare this to log.match(/.../g), which returns an array of whole matches and discards the groups. That difference is the reason matchAll exists.

matchAll returns an iterator rather than an array, so it is lazy in the sense of lesson 7-2 and can be spread with [...log.matchAll(re)] when you want a real array.

flip

Two groups and a reordered replacement.

function flip(name) {
  return name.replace(/(\w+), (\w+)/, "$2 $1");
}

console.log(flip("Lovelace, Ada"));
console.log(flip("Hopper, Grace"));

Output

Ada Lovelace
Grace Hopper

The pattern captures the last name with (\w+), matches the literal comma and space, then captures the first name.

The replacement string is "$2 $1", meaning the second group, a space, and the first group.

replace returns a new string and never mutates the input, which is true of every string method in JavaScript.

A name that does not match is returned unchanged, so flip("Ada") gives "Ada". That is often the behavior you want, and it also means a malformed input fails silently.

\w covers letters, digits, and the underscore, so hyphenated or accented names slip through unmatched. Widening it to ([^,]+), (.+) is the pragmatic fix for real name data.