Regex in JavaScript

The literal and constructor forms, the match API you actually use, replacements with groups, and the global lastIndex trap.

Literal versus constructor

const literal = /a+b/i;                       // compiled once, flags fixed
const dynamic = new RegExp('a+b', 'i');       // built at runtime

// escaping the hard way: a user-supplied search term is not a pattern
function escapeRegExp(text) {
  return text.replace(/[.*+?^()|[\]\\{}$#!]/g, '\\$&');
}

const userInput = 'price (net)';
new RegExp(escapeRegExp(userInput)).test('the price (net) today');   // true
new RegExp(userInput).test('the price (net) today');                 // throws: unbalanced group
  • Use the literal form for any pattern you write by hand: it is compiled once and it cannot be changed by a caller.
  • Use the constructor only when part of the pattern is dynamic, and always escape the dynamic part first.
  • Two regex literals with the same source are distinct objects. Do not compare them with ===; compare source and flags.

test, exec, matchAll and replace

MethodReturnsUse it when
re.test(s)booleanyou only need yes or no
re.exec(s)match array or nullyou need groups from one match
s.match(re)array, or all matches with gyou want the matched text and no groups
s.matchAll(re)an iterator of match arraysyou need groups from every match
s.replace(re, fn)a new stringyou transform what you matched
s.split(re)an arrayyou split on a pattern, not one character
const log = '2026-09-18 INFO ok\n2026-09-18 ERROR failed';

// groups for every match: use matchAll, not a while loop over exec
for (const m of log.matchAll(/^(\S+) (\w+) (.+)$/gm)) {
  console.log(m[1], m[2], m[3].trim());
}

// string replacement with numbered groups
'2026-09-18'.replace(/(\d+)-(\d+)-(\d+)/, '$3/$2/$1');   // '18/09/2026'

// a function gives you full control over every match
'hello world'.replace(/\b(\w)/g, (full, ch) => ch.toUpperCase());   // 'Hello World'

// the trailing arguments are the offset and the whole subject
'hello world'.replace(/\b\w+/g, (word, offset, subject) =>
  offset === 0 ? word.toUpperCase() : word);                          // 'HELLO world'

// split on any run of separators
'a,,b ; c'.split(/[\s,;]+/);        // [ 'a', 'b', 'c' ]
  • matchAll requires the g flag and returns an iterator; it is the only clean way to get groups from every match.
  • String.prototype.match with g discards captures entirely, which surprises people constantly.
  • In a replacement string, $& is the whole match, $1 to $99 are groups, $<name> is a named group, and $$ is a literal dollar sign.

The lastIndex trap

A regex object with the g or y flag carries a lastIndex that every exec and test call updates. A module-level regex reused by several callers therefore returns different answers depending on call order.

const re = /\d/g;

re.test('1');       // true,  lastIndex is now 1
re.test('1');       // false, the search starts at index 1
re.test('1');       // true,  lastIndex wrapped back to 0

// correct patterns
const fresh = /\d/g;
if (fresh.test(s)) fresh.lastIndex = 0;        // reset explicitly

const sticky = /\d/y;                          // y matches only at lastIndex
sticky.lastIndex = 2;
sticky.test('ab3');                            // true
sticky.test('ab3');                            // false, lastIndex advanced past the end
⚠️
Never leave a g or y regex at module scope if you call test or exec on it. Either create the regex inside the function, or use matchAll, which manages the index for you and leaves it where you would expect.

FAQ

Why does the same test return true and then false?
The regex has the g flag and its lastIndex advanced after the first call, so the second search starts where the first stopped. Create a fresh regex per call or reset lastIndex to 0.
How do I get all groups from all matches?
Use string.matchAll(/pattern/g) and iterate the result. string.match() with the g flag returns only the matched text and drops every capture group.

Flags and matching modes Text surgery: find and replace across a codebase

Last refreshed 2026-09-18.