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
===; comparesourceandflags.
test, exec, matchAll and replace
| Method | Returns | Use it when |
|---|---|---|
re.test(s) | boolean | you only need yes or no |
re.exec(s) | match array or null | you need groups from one match |
s.match(re) | array, or all matches with g | you want the matched text and no groups |
s.matchAll(re) | an iterator of match arrays | you need groups from every match |
s.replace(re, fn) | a new string | you transform what you matched |
s.split(re) | an array | you 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' ]matchAllrequires thegflag and returns an iterator; it is the only clean way to get groups from every match.String.prototype.matchwithgdiscards captures entirely, which surprises people constantly.- In a replacement string,
$&is the whole match,$1to$99are 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.Related
Flags and matching modes Text surgery: find and replace across a codebase
Last refreshed 2026-09-18.