Two ways to make one
const re = /\d{4}/; // literal — compiled once
const re = new RegExp("\\d{4}", "g"); // from a string: every backslash doubles
// build one safely from user input
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const find = new RegExp(escape(query), "gi");
The pieces you will actually use
| Pattern | Matches |
|---|---|
. | Any character except a newline |
\d \w \s | Digit, word character, whitespace |
\D \W \S | The opposite of each |
[abc] [^abc] [a-z] | A set, a negated set, a range |
* + ? | Zero or more, one or more, optional |
{3} {2,} {2,5} | Exactly, at least, between |
^ $ | Start and end of the string (or of a line with m) |
\b | A word boundary |
(…) | A capturing group |
(?:…) | A group that does not capture |
(?<name>…) | A named group |
a|b | Either |
Flags
/x/g // global — find all, not just the first
/x/i // case-insensitive
/x/m // ^ and $ match at line breaks
/x/s // . also matches a newline
/x/u // full unicode — needed for \p{…} and for astral characters
Using one
re.test(str); // boolean — the one you want most often
str.match(/\d+/); // the first match with its groups, or null
str.match(/\d+/g); // ALL the matches as plain strings, or null
[...str.matchAll(/(\w+)=(\w+)/g)]; // every match WITH its groups
str.replace(/-/g, "+");
str.split(/\s*,\s*/);
re.test(s) with the g flag advances re.lastIndex, so calling it twice on the same string alternates true and false. Never reuse a g regex with test — drop the flag, or reset lastIndex.
Groups
const m = "2026-09-21".match(/(\d{4})-(\d{2})-(\d{2})/);
m[0]; // "2026-09-21" — the whole match
m[1]; // "2026"
const { groups } = "2026-09-21".match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/);
groups.y; // "2026"
// groups in a replacement
"2026-09-21".replace(/(?<y>\d+)-(?<m>\d+)-(?<d>\d+)/, "$<d>/$<m>/$<y>");
Greedy and lazy
? after a quantifier makes it lazy. Most "why did my regex eat the whole line" questions are a greedy . that should have been .? — or should not have been . at all.
'<a><b>'.match(/<.+>/)[0]; // "<a><b>" — greedy: as much as possible
'<a><b>'.match(/<.+?>/)[0]; // "<a>" — lazy: as little as possible
A replacement function
"5 doors, 3 windows".replace(/(\d+) (\w+)/g, (m, n, word) =>
`${Number(n) * 2} ${word}`
);
// "10 doors, 6 windows"
// a safe highlighter
const highlight = (text, q) => {
const re = new RegExp(escape(q), "gi");
return text.replace(re, (m) => `<mark>${m}</mark>`); // escape the TEXT first!
};
What not to match with a regex
- Email addresses. The valid grammar is far stranger than any regex people write. Use
type="email"and send a confirmation message — that is the only real test. - HTML. Nested, optional and recoverable. Parse it:
new DOMParser().parseFromString(html, "text/html"). - URLs.
new URL(str)in atrytells you whether it is valid and gives you the parts. - Dates.
new Date(iso)and checkisNaN. - JSON.
JSON.parsein atry.
A pattern with nested quantifiers — (a+)+b — can take exponential time on a string that nearly matches. If a regex comes from user input, or has nested + and *, that is a denial-of-service waiting to happen.
Keeping them readable
A regex is write-once and read-never unless you give it a name. Two named constants at the top of a file are worth more than any amount of cleverness inside them.
// a four-digit year, a two-digit month, a two-digit day
const ISO_DATE = /^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})$/;
// rather than this, inline, three times
if (/^(\d{4})-(\d{2})-(\d{2})$/.test(s)) { }