JavaScript Regex Cheat Sheet
Every JavaScript regex token that matters — anchors, classes, quantifiers, groups, lookarounds, flags and substitutions — in scannable tables.
This is the JavaScript dialect — what new RegExp() in a browser or Node.js understands. Most of it is shared with PCRE and Python, but the differences (ASCII \w, no possessive quantifiers, the u flag changing what’s legal) are marked where they matter. Try any row live on the tester home page.
Anchors — positions, not characters
| Token | Matches | JS notes |
|---|---|---|
^ |
start of input | with m: also after every \n |
$ |
end of input | with m: also before every \n; without it, still allows one final \n |
\b |
word boundary | between \w and non-\w — matches nothing itself |
\B |
non-word-boundary | everywhere \b doesn’t |
There is no \A or \z in JavaScript — ^/$ are all you get, which is why the m flag changes their meaning rather than adding separate tokens.
Character classes
| Token | Matches | Notes |
|---|---|---|
. |
any char except line terminators | with s: truly anything |
\d \D |
digit / non-digit | [0-9] — ASCII only, always |
\w \W |
word / non-word | [a-zA-Z0-9_] — stays ASCII even under u |
\s \S |
whitespace / non-whitespace | includes \n \t \r \f \v and Unicode spaces |
[abc] |
one of a, b, c | ] ^ - \ need care inside |
[^abc] |
one char NOT a, b, c | negation |
[a-z] |
range | [0-9a-fA-F] is the hex idiom |
\p{L} |
any Unicode letter | needs the u flag — also \p{N}, \p{Emoji}, \p{Script=Greek} |
Quantifiers — and the laziness toggle
| Token | Meaning | Greedy vs lazy |
|---|---|---|
* |
0 or more | greedy: max first, back off if needed |
+ |
1 or more | same |
? |
optional | greedy — prefers “present” |
{n} |
exactly n | |
{n,} |
n or more | unbounded — watch these inside loops |
{n,m} |
between n and m |
Append ? to any quantifier to make it lazy (match as little as possible): *?, +?, ??, {2,}?. JavaScript has no possessive quantifiers (a++) and no atomic groups ((?>…)) — the constructs other engines offer to kill backtracking. That’s exactly why catastrophic backtracking is a JavaScript problem, not just a theory problem.
Groups — capturing, not capturing, named
| Syntax | What it does |
|---|---|
(abc) |
capture group — saves matched text as group n (\n, $n, match[n]) |
(?:abc) |
non-capturing — grouping only, nothing saved |
(?<name>abc) |
named capture — match.groups.name, \k<name>, $<name> in replace |
\1–\9 |
backreference — match the same text group n already captured |
a|b |
alternation — left branch tried first, order matters |
Lookarounds — zero-width requirements
| Syntax | Meaning |
|---|---|
x(?=y) |
x only if followed by y (lookahead, consumes nothing) |
x(?!y) |
x only if NOT followed by y |
(?<=y)x |
x only if preceded by y (lookbehind, ES2018) |
(?<!y)x |
x only if NOT preceded by y |
Lookbehind is the newest and the one to check if you ship to older environments. The classic use: \d+(?= dollars) matches the number in 100 dollars without capturing the unit.
Flags — what each one actually changes
| Flag | Name | Effect |
|---|---|---|
g |
global | find all matches, not just the first — also makes exec/test stateful via lastIndex |
i |
ignoreCase | case folding (respects u Unicode folding) |
m |
multiline | ^/$ match per line |
s |
dotAll | . matches line terminators too |
u |
unicode | code-point semantics, \p{}, \u{}, stricter syntax |
y |
sticky | match must start exactly at lastIndex — no skipping ahead |
d |
hasIndices | match objects carry [start,end] per group (ES2022) |
Replace substitutions — the $ tokens
| Token | Inserts |
|---|---|
$$ |
a literal $ |
$& |
the whole match |
$` |
text before the match |
$' |
text after the match |
$1…$99 |
capture group text |
$<name> |
named group text |
'2026-09-24'.replace(/(\d+)-(\d+)-(\d+)/, '$3/$2/$1') → '24/09/2026'. Try it in the replace mode on the tester.
Idioms worth memorizing
[\s\S]— “any character including newlines” without thesflag[^"]*— run of non-quote chars (safer than.*?when you know the delimiter)\bword\b— whole-word match (\bfails next to punctuation, not inside\w)(?<=\d)px— a unit that must follow a digit, excluded from the match^(?:.*\n){3}— “exactly the first three lines” is not this — but it shows how groups compose
When in doubt, paste the pattern into the explain mode — it reads every token back to you in English before you ship it.
Frequently asked questions
Is \w the same as [a-zA-Z0-9_]?
Almost — in JavaScript without the u flag, \w is exactly [a-zA-Z0-9_]: ASCII only, so it won't match é, ü or Chinese characters. With the u flag it stays ASCII (unlike some engines where \w goes Unicode) — for real Unicode letters you need \p{L} with the u flag, e.g. /[\p{L}\p{N}_]/u.
Why doesn't my dot match newlines?
It never did — . in JavaScript means "any character except a line terminator" (\n, \r, \u2028, \u2029). Add the s flag (dotAll, ES2018) to make . truly mean anything, or use the idiom [\s\S] which has worked forever.
What's the difference between *? and +?
One is about how MUCH to match, the other about HOW. + is greedy: take as many as possible, back off only if the rest of the pattern fails. *? is lazy: take as few as possible, extend only when forced. <.+> on '' grabs '' in one match; <.+?> grabs '' then ''.
Do I need the u flag?
If your text can contain emoji, non-Latin scripts or combining marks — yes. Without u, JavaScript counts UTF-16 code units, so '💩'.length is 2 and . can't match it in one token. u switches to code points and unlocks \p{Letter}, \u{1F4A9} and proper case folding. Cost: stricter syntax — lone {, ] and - in classes become errors.
How do I match a literal special character?
Escape it with a backslash: \. \* \+ \? \( \) \[ \] \{ \} \^ \$ \| \\ and \/ inside a /literal/. Inside a character class you need fewer escapes — [.*+] works — but ], ^, - and \ still need care.
Why does /\d+/.test('42') work but /\d+/g.test() twice give different results?
The g flag makes a RegExp stateful: exec() and test() resume from re.lastIndex, and a successful test leaves lastIndex at the end of the match — the next call starts mid-string. It's a real bug source; use matchAll() for iteration or reset re.lastIndex = 0 between uses.