Regular expressions in JavaScript: the practical parts
A snip.tools guide · runs alongside the Regex tester
Prefer to just run it? Use the free Regex tester — no install, in your browser.
Open the Regex testerRegular expressions are a tiny language for describing patterns in text — validating input, extracting fields, search-and-replace. JavaScript has them built into the language, but the API has a few sharp edges (a stateful global flag, escaping rules) that cause most of the confusion. This guide covers the parts you actually use day to day. (To build and try a pattern against live sample text, our in-browser regex tester shows every match and group as you type.)
Creating a regex: literal vs constructor
There are two ways to make a RegExp. A literal between slashes is compiled once
and is what you want for a fixed pattern. The constructor builds one from a string, which
matters only when the pattern is dynamic — and forces you to double every backslash, since the string parser
eats one layer first:
// Two ways to make the same regex.
const literal = /\d{3}-\d{4}/; // compiled once, best for fixed patterns
const built = new RegExp('\\d{3}-\\d{4}'); // from a string — note the doubled backslashes
// The constructor is for when the pattern is dynamic (built at runtime). The four methods that matter
A regex is used through a handful of methods split across RegExp and String. The
ones you'll reach for:
const re = /\d+/g;
const s = 'order 42, item 7';
re.test(s); // true — is there a match?
s.match(/\d+/); // ["42"] (first match, no /g)
s.match(re); // ["42", "7"] (all matches, with /g)
[...s.matchAll(re)]; // full match objects, incl. index + groups
s.replace(re, '#'); // "order #, item #"
Rules of thumb: test() for a yes/no check; match() for a quick first hit;
matchAll() when you want every match with its groups and positions; and
replace() for transformation. Note that match() with the /g flag
returns just the strings — to get capture groups for every match, use matchAll().
The flags
Flags go after the closing slash and change how the whole pattern behaves:
/cat/i // i — case-insensitive
/^x/m // m — ^ and $ match per line, not just whole string
/a.b/s // s — . also matches newlines ("dotAll")
/\d+/g // g — find all matches, not just the first
/\p{Emoji}/u // u — full Unicode + enables \p{...} property escapes
The two most consequential are g (all matches, not the first — but it makes the regex stateful,
see below) and u, which you should add whenever you work with non-ASCII text, because it turns
on correct Unicode handling and the \p{…} property escapes.
Capture groups
Parentheses capture the part of the match they enclose so you can pull it out. Named groups make the intent obvious and don't break when you reorder the pattern:
// Capture groups pull pieces out of a match.
const m = '2026-06-22'.match(/(\d{4})-(\d{2})-(\d{2})/);
m[1]; // "2026" m[2]; // "06" m[3]; // "22"
// Named groups are clearer and survive refactoring:
const d = '2026-06-22'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
d.groups.year; // "2026"
// Reference them in replace with $<name>:
'2026-06-22'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<d>/$<m>/$<y>');
// "22/06/2026"
If you want grouping for an alternation like (?:cat|dog) but don't need to capture it, use a
non-capturing group (?:…) — it keeps your numbered groups clean.
The biggest gotcha: lastIndex and the /g flag
This one wastes hours. A regex with the g (or sticky y) flag is stateful:
it stores a lastIndex and resumes from there on the next call. Reuse the same /g regex object across test() calls and you get alternating true/false:
// A /g (or /y) regex is STATEFUL — it remembers where it left off.
const re = /\d+/g;
re.test('a1'); // true, lastIndex now 2
re.test('a1'); // FALSE — it resumed past the match, found nothing
re.lastIndex = 0; // reset, or just don't reuse a /g regex across calls
The fixes: don't put /g on a regex you call test() with repeatedly; reset
lastIndex = 0 between uses; or define the regex literal inline so a fresh one is created each
time. This is also why a module-level const re = /…/g shared across functions is a footgun.
Escape dynamic input
When you build a regex from a string the user supplied, escape it first. Otherwise characters like
., ( or * are interpreted as operators — at best you match the wrong
thing, at worst you create a catastrophic-backtracking pattern (ReDoS) that hangs on certain inputs:
// Escape user input before putting it in a RegExp, or characters
// like . ( ) [ ] become operators (and a bad pattern can hang — see ReDoS).
function escapeRegExp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const re = new RegExp(escapeRegExp(userInput), 'g'); A word on "validation" patterns
Be skeptical of copy-pasted "validate an email/URL/phone" regexes. A fully RFC-compliant email regex is
monstrous and still can't confirm an address exists; a short pragmatic check like
/^.+@.+\..+$/ plus an actual verification email is usually the better engineering call. Regex
is for shape, not truth. Build and debug your pattern against real sample text in the
regex tester — it highlights matches and named groups live, entirely in your
browser. Once your pattern extracts the fields you need, tools like the
JSON formatter help you inspect the structured result.
Try it now: the Regex tester does everything in this guide in your browser — nothing is uploaded. Browse more guides or the full tool list.