Regex in JavaScript
JavaScript's regex API has a few sharp edges that catch people. Here's what to use when, and what to avoid.
Literal vs constructor
const a = /\d+/g; // literal
const b = new RegExp("\\d+", "g"); // constructor
Literals are parsed once at script load. Constructors run at evaluation time, so use them when the pattern comes from a variable. Don't forget: in the constructor form, the string sees backslashes first — you need to double them.
Test vs match vs matchAll
regex.test(str) returns true/false. Fast — use it when you just need to know if there's a match.
str.match(regex):
- Without
/g: returns the first match with capture groups, ornull. - With
/g: returns an array of full-match strings only — capture groups are lost.
str.matchAll(regex) requires /g, returns an iterator of full match objects (with groups). Use this when you need both global iteration and capture groups.
const text = "phone: 555-1234, fax: 555-5678";
// Just check
/\d{{3}}-\d{{4}}/.test(text); // true
// First match with groups
text.match(/(\d{{3}})-(\d{{4}})/); // ["555-1234", "555", "1234"]
// All matches as strings
text.match(/\d{{3}}-\d{{4}}/g); // ["555-1234", "555-5678"]
// All matches with groups
[...text.matchAll(/(\d{{3}})-(\d{{4}})/g)]; // [["555-1234", "555", "1234"], ...]
The lastIndex trap
A regex object with /g or /y remembers where it left off via lastIndex. Subsequent test or exec calls continue from there.
const re = /\d/g;
re.test("a1b"); // true, lastIndex now 2
re.test("a1b"); // false! Started search at index 2
re.test("a1b"); // true again — lastIndex reset to 0 on no-match
The fix: don't keep a stateful global regex if you're calling test repeatedly. Either reset lastIndex = 0, or drop the g flag.
Replace
str.replace(regex, replacement) with a global regex replaces every match. With a non-global, only the first.
Replacement strings have their own metacharacters:
$& the whole match
$1, $2 capture groups
$<name> named group (ES2018+)
$$ a literal dollar sign
$` text before the match
$' text after the match
Replacement as function — runs once per match:
"hello world".replace(/(\w+)/g, (match, group1) => group1.toUpperCase());
// "HELLO WORLD"
String.split with regex
str.split(regex) splits the string by every regex match. Capture groups in the regex are included in the output array — useful for tokenizers.
"a-1-b-2-c".split(/-/); // ["a", "1", "b", "2", "c"]
"a-1-b-2-c".split(/(-)/); // ["a", "-", "1", "-", "b", "-", "2", "-", "c"]
Always use /u for non-ASCII
Without the u flag, JavaScript treats surrogate pairs (emoji, astral plane Unicode) as two characters. /.{{1}}/.test("👍") is true because . matched one of the two surrogate halves. /.{{1}}/u.test("👍") is false because . now matches the whole code point.
If you ever handle user-supplied text that might contain emoji or non-Latin scripts, default to u.
RegExp.escape is coming, but not yet
If you need to embed a literal string in a regex, you have to escape its metacharacters yourself:
function escape(s) {{
return s.replace(/[.*+?^${{}}()|[\]\\]/g, '\\$&');
}}
new RegExp(escape(userInput));
A RegExp.escape static method is in the TC39 pipeline. For now, every project has its own escape helper.