JavaScript Regular Expressions: A Practical Cheat Sheet
A JavaScript regular expression is a pattern written as /pattern/flags or built with new RegExp('pattern', 'flags'), and used with methods such as test(), match(), matchAll() and replace(). This cheat sheet lists the syntax you reach for most often, then gives eleven practical patterns that were each run in Node.js 20 against inputs that should and should not match, with their limits stated. You can try any of them in the Regex Tester.
Character classes
| Syntax | Matches |
|---|---|
. | Any character except line terminators (unless the s flag is set) |
\d / \D | An ASCII digit 0–9 / anything else |
\w / \W | An ASCII letter, digit or underscore / anything else |
\s / \S | Whitespace, including spaces, tabs, line breaks and Unicode spaces / anything else |
[abc] | One of a, b or c |
[^abc] | Any character except a, b or c |
[a-z0-9] | A character in either range |
\p{L}, \p{Lu} | A Unicode letter, an uppercase letter (needs the u or v flag) |
\d, \w and \b are ASCII-only even with the u flag: 'café latte'.match(/\b\w+\b/g) returns ['caf', 'latte']. Use \p{L} with u when you need letters from any script.
Anchors and boundaries
| Syntax | Matches at |
|---|---|
^ | Start of input, or start of any line with the m flag |
$ | End of input, or end of any line with the m flag |
\b | A word boundary, between a \w and a \W character |
\B | Any position that is not a word boundary |
Unlike some other engines, a JavaScript $ without m does not match before a trailing newline: /^\d{3}$/.test('123\n') is false.
Quantifiers: greedy and lazy
| Greedy | Lazy | Repeats |
|---|---|---|
* | *? | 0 or more |
+ | +? | 1 or more |
? | ?? | 0 or 1 |
{n} | – | Exactly n |
{n,} | {n,}? | n or more |
{n,m} | {n,m}? | Between n and m |
A greedy quantifier takes as much as it can and gives characters back only if the rest of the pattern fails; a lazy one takes as little as possible.
'<b>x</b><b>y</b>'.match(/<b>.*<\/b>/)[0] // '<b>x</b><b>y</b>'
'<b>x</b><b>y</b>'.match(/<b>.*?<\/b>/)[0] // '<b>x</b>'
Groups and backreferences
| Syntax | Meaning |
|---|---|
(abc) | Capturing group, numbered from 1 by its opening bracket |
(?:abc) | Non-capturing group: groups for quantifiers or alternation without capturing |
(?<name>abc) | Named capturing group, available as match.groups.name |
\1, \k<name> | Backreference: the same text the group captured |
a|b | Alternation: a or b |
$1, $<name>, $& | In a replace() string: group 1, a named group, the whole match |
const m = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec('Due 2026-10-03');
m.groups.year; // '2026'
'2026-10-03'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<d>/$<m>/$<y>'); // '03/10/2026'
Lookahead and lookbehind
Lookarounds test what comes before or after a position without including it in the match.
| Syntax | Meaning | Example |
|---|---|---|
X(?=Y) | X followed by Y | /\w+(?=\.js$)/ on file.test.js gives test |
X(?!Y) | X not followed by Y | /q(?!u)/gi on Iraq quit matches only the q in Iraq |
(?<=Y)X | X preceded by Y | /(?<=£)\d+/g on £30, €45, £7 gives 30, 7 |
(?<!Y)X | X not preceded by Y | /(?<!\$)\d/g on a1 $2 b3 gives 1, 3 |
Flags
| Flag | Name | Effect |
|---|---|---|
g | global | Find all matches; required by matchAll() and replaceAll() with a regex |
i | ignoreCase | Case-insensitive matching |
m | multiline | ^ and $ match at line starts and ends |
s | dotAll | . also matches line terminators (ES2018) |
u | unicode | Code-point matching, \p{…} escapes, stricter syntax (ES2015) |
y | sticky | Match only at exactly lastIndex (ES2015) |
d | hasIndices | Adds start and end positions of each group as match.indices (ES2022) |
v | unicodeSets | An upgraded u with set operations such as [\p{L}--[a-z]] (ES2024); cannot be combined with u |
Node.js 20 accepts all eight; d and v are the newest, so check your target browsers before relying on them. Inline modifiers such as (?i:…) and the RegExp.escape() function are more recent still and are not available in Node.js 20. The Regex Tester on this site has checkboxes for g (on by default), i, m, s and u.
Escaping
These characters have special meaning and need a backslash to match literally: . * + ? ^ $ { } ( ) | [ ] \ / (the slash only inside a /…/ literal). Inside a character class (without the v flag), only \, ], and ^ at the start or - between characters, are special. To use arbitrary text as a literal pattern:
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
new RegExp(escapeRegExp('1.5*(x)')).test('cost 1.5*(x)'); // true
Practical patterns, tested
Each pattern below was run in Node.js 20 with the positive and negative examples shown.
| Task | Pattern | Matches | Rejects |
|---|---|---|---|
| ISO 8601 calendar date | /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/ | 2026-10-03, 2024-02-29 | 2026-13-01, 2026-1-3, 2026/10/03 |
| Hex colour | /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i | #fff, #4F46E5, #4f46e5cc | fff, #fffff, #ggg |
| Email sanity check | /^[^\s@]+@[^\s@]+\.[^\s@]+$/ | [email protected], [email protected] | hello@example, @example.com, hello@@example.com |
| IPv4 address | /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/ | 192.168.0.1, 255.255.255.255 | 256.1.1.1, 1.2.3, 01.2.3.4 |
| Semantic version (simplified) | /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/ | 1.2.3, 1.0.0-rc.1+sha.abc | 1.2, 01.2.3, v1.2.3 |
| Repeated word | /\b(\w+)\s+\1\b/gi | this is is a test, The the cat | this is a test, is island |
Limitations, honestly stated:
- Date: checks shape, not the calendar, so
2023-02-31passes. Confirm with a date library or by round-tripping throughDate. - Email: this only catches obvious typos. It accepts
[email protected]and rejects the RFC 5322-valid"john smith"@example.com. Fully validating RFC 5322 addresses with a regex is impractical; the only real test is sending a confirmation message. - IPv4: rejects leading zeros on purpose, because some parsers read them as octal. It does not handle IPv6.
- Semantic version: accepts some prerelease strings the specification forbids, such as
1.0.0-01. semver.org publishes a full pattern if you need strictness. - Repeated word:
\wis ASCII-only, so accented words are not handled.
Replacement and extraction patterns
// Trim whitespace at both ends (String.prototype.trim() does the same)
' \t hello world \n '.replace(/^\s+|\s+$/g, ''); // 'hello world'
// Collapse runs of whitespace, including newlines, to one space
'a b\t\tc\n d'.replace(/\s+/g, ' '); // 'a b c d'
// Remove duplicated words
'Paris in the the spring'.replace(/\b(\w+)\s+\1\b/gi, '$1'); // 'Paris in the spring'
// Thousands separators for an integer string
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ','); // '1,234,567'
// URL slug
' Cron Syntax: Explained! '.toLowerCase()
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); // 'cron-syntax-explained'
// Query parameters
const url = 'https://example.com/search?q=cron%20syntax&page=2&debug#top';
[...url.matchAll(/[?&]([^=&#]+)=?([^&#]*)/g)].map(m => [m[1], m[2]]);
// [['q', 'cron%20syntax'], ['page', '2'], ['debug', '']]
The thousands pattern breaks on decimals (1234.5678 becomes 1,234.5,678), so prefer Intl.NumberFormat for real numbers. The slug pattern drops accented letters (Crème brûlée becomes cr-me-br-l-e), so normalise text first if that matters. The query-string pattern does not percent-decode values; new URL(url).searchParams does, and handles edge cases a regex will miss.
Common pitfalls
Catastrophic backtracking
Nested quantifiers that can match the same text in many ways make the engine try every combination before failing. /^(a+)+$/ tested against a run of a characters followed by ! roughly doubles its running time with every extra a; in our Node.js 20 test, 28 characters already took several seconds, while the equivalent /^a+$/ answered instantly. Avoid patterns such as (x+)+, (x|x)* or (.*,)*, and never run user-supplied patterns on a server without a time limit. The Regex Tester re-runs your pattern in the page on every keystroke, so a pattern like this can make the browser tab unresponsive.
Forgetting the g flag
replace() with a non-global regex changes only the first match ('a1b2'.replace(/\d/, '#') gives 'a#b2'). matchAll() and replaceAll() throw a TypeError if the regex lacks g.
lastIndex with g or y and test()
A global or sticky regex remembers where it stopped in lastIndex, so reusing it gives alternating results:
const re = /cat/g;
re.test('cat'); // true (lastIndex is now 3)
re.test('cat'); // false (search starts at 3, fails, lastIndex resets to 0)
re.test('cat'); // true
Drop the g flag for yes/no checks, or reset re.lastIndex = 0 before each call.
Escaping inside new RegExp strings
A string literal consumes one level of backslashes before the regex engine sees it. new RegExp('\d+') produces the pattern d+, matching the letter d. Write new RegExp('\\d+') or new RegExp(String.raw`\d+`). In the Regex Tester you type the pattern itself, without surrounding slashes or string escaping.
Using the Regex Tester
Type a pattern and test string and the tester highlights every match as you type, with a match count in the Test string header. The Matches panel lists each match with its number, its index in the text and the captured groups as a JSON array in numeric order; named groups appear by position, and a group that did not take part in the match shows as null. With g unticked only the first match is listed. Filling in Replace with switches the panel to a replacement result, using JavaScript's replace(), so $1, $<name> and $& all work.
Frequently asked questions
What is the difference between test(), match() and exec()?
test() returns true or false. exec() returns one match with its groups and index, advancing lastIndex with g or y. match() returns the first match with groups, or with g an array of every matched string without groups; use matchAll() to get every match with its groups.
Does JavaScript support lookbehind?
Yes. Both (?<=…) and (?<!…) are part of ES2018 and work in current browsers and Node.js.
How do I make the dot match newlines?
Add the s flag, or use [\s\S] in engines that predate it.
Can I validate an email address with a regex?
Only roughly. A simple pattern catches typos such as a missing @, but the full address grammar is too complex to express practically, and a well-formed address may still not exist. Send a verification email for anything that matters.