Regex in JavaScript
Every Method Explained with Examples
Pick the right method, read the return value, and never hit a mysterious null again.
Regex in JavaScript gives you six distinct methods across RegExp and String, each returning something different — and picking the wrong one is the most common source of "why did my regex return null?" bugs.
Quick-Reference: All Seven Methods
Before diving in, here is every regex-aware method at a glance — the object it lives on, whether it needs the /g flag, and what it hands back.
test(str)
exec(str)
/g required
match(regex)
matchAll(regex)
/g required
replace(regex, x)
replaceAll(regex, x)
/g required
split(regex)
test() — The Simplest Check
RegExp.test(str) returns true or false. Use it whenever you only need to know whether a match exists.
// Basic use — just need a boolean
const isHex = /^#[0-9a-f]{6}$/i;
isHex.test('#1a2b3c'); // true
isHex.test('red'); // false
// ⚠ Stateful lastIndex bug — DON'T do this
const re = /\d+/g;
['abc', '123', '456'].filter(s => re.test(s));
// Returns ['123'] — '456' is missed because lastIndex is non-zero!
// ✓ Fix 1 — drop the /g flag when you don't need it
const re2 = /\d+/;
['abc', '123', '456'].filter(s => re2.test(s));
// Returns ['123', '456'] — correct
// ✓ Fix 2 — reset lastIndex before each call if /g is genuinely needed
re.lastIndex = 0;
re.test('next string');
match() — With and Without /g
String.match(regex) behaves very differently depending on whether the /g flag is present. Without /g you get the first match plus capture groups. With /g you get all full matches — but capture groups are silently dropped.
const str = 'prices: $12.50 and $7.99';
// WITHOUT /g — first match + capture groups
str.match(/\$(\d+\.\d{2})/);
// ['$12.50', '12.50', index: 8, input: '...', groups: undefined]
// ↑ full match ↑ group 1
// WITH /g — all full matches, capture groups gone
str.match(/\$(\d+\.\d{2})/g);
// ['$12.50', '$7.99'] ← no group 1 values
// Returns null when there is no match — always guard it
const m = str.match(/€\d+/);
if (m) { /* safe to use m[0], m[1] etc. */ }
matchAll() — All Matches With Capture Groups
String.matchAll(regex) fixes the biggest frustration with match(): it preserves capture groups for every match, not just the first. It requires the /g flag and returns a lazy iterator.
const str = '2026-01-15 and 2026-03-22';
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
// Spread into an array
const matches = [...str.matchAll(re)];
matches[0][0] // '2026-01-15' full match
matches[0][1] // '2026' group 1
matches[0].groups.month // '01' named group
// Iterate with destructuring — cleaner
for (const m of str.matchAll(re)) {
const { year, month, day } = m.groups;
console.log(`${year}/${month}/${day}`);
}
// 2026/01/15
// 2026/03/22
exec() — Low-Level and Stateful
RegExp.exec(str) is the oldest regex method in JavaScript. Each call returns one match (the next one when /g is set), or null when exhausted. It predates matchAll() and the classic use-case is a while loop.
const str = 'cat bat sat';
const re = /[a-z]at/g;
let m;
// Classic while-loop pattern
while ((m = re.exec(str)) !== null) {
console.log(m[0], 'at index', m.index);
}
// cat at index 0
// bat at index 4
// sat at index 8
// ✓ Prefer matchAll() for modern code — same result, cleaner
for (const m of str.matchAll(/[a-z]at/g)) {
console.log(m[0], 'at index', m.index);
}
replace() and replaceAll()
Both methods return a new string — the original is never mutated. The replacement argument can be a string (with backreferences) or a function called for each match.
// Replace first vs all
'aabbaa'.replace(/a/, 'x') // 'xabbaa' — first only
'aabbaa'.replace(/a/g, 'x') // 'xxbbxx' — all
'aabbaa'.replaceAll(/a/g, 'x') // 'xxbbxx' — all (requires /g)
// Backreferences $1 $2 in the replacement string
'2026-05-02'.replace(
/(\d{4})-(\d{2})-(\d{2})/,
'$3/$2/$1'
);
// '02/05/2026'
// $& inserts the entire matched substring
'hello world'.replace(/\w+/g, '[$&]');
// '[hello] [world]'
// Replacement function — full control per match
'prices: $12.50 and $7.99'.replace(
/\$(\d+\.\d{2})/g,
(match, p1, offset) => {
const doubled = (parseFloat(p1) * 2).toFixed(2);
return `$${doubled}`;
}
);
// 'prices: $25.00 and $15.98'
split() — Splitting on a Pattern
String.split(regex) treats every match as a delimiter. Capture groups inside the regex are included in the output array — useful when you need to keep the delimiters.
// Split on one or more whitespace characters
' foo bar baz '.split(/\s+/);
// ['', 'foo', 'bar', 'baz', ''] ← empty strings at edges
// Split on any punctuation between words
'one,two;three four'.split(/[,;\s]+/);
// ['one', 'two', 'three', 'four']
// Capture groups are included in the result
'2026-05-02'.split(/(-)/);
// ['2026', '-', '05', '-', '02']
// ↑ the delimiter itself is kept
Flags Reference
| Flag | Name | Effect |
|---|---|---|
| /g | global | Find all matches; makes exec()/test() stateful via lastIndex. |
| /i | ignoreCase | Case-insensitive matching — A matches a, Z matches z. |
| /m | multiline | ^ and $ match start/end of each line, not just the full string. |
| /s | dotAll | . matches any character including newline \n (ES2018). |
| /u | unicode | Full Unicode mode; required for \p{…} property escapes and astral codepoints. |
| /d | hasIndices | Adds .indices property to matches with start/end positions of each group (ES2022). |
Common Mistakes
Test Your Regex in Real Time
Paste any pattern from this page into the Regex Tester — see matches, capture groups, and flags highlighted instantly, all in your browser.
Open Regex Tester →