JavaScript Regular Expressions
A regular expression (regex) is a tiny pattern language for matching, validating, extracting, and replacing text. JavaScript has first-class regex syntax (/pattern/flags) and a complete API on both RegExp and String. This chapter covers the syntax you’ll actually use, all the flags, groups (including named and non-capturing), lookaround, the String methods, and a few real-world patterns.
Two Ways to Create
const a = /hello/i; // literal
const b = new RegExp("hello", "i"); // from a string
Use the literal for static patterns and new RegExp(str, flags) when the pattern is built at runtime. With the constructor you must double-escape backslashes — "\\d" for \d.
Quick Tests
/hello/i.test("HELLO World"); // Output: true
"hello world".match(/world/); // Output: ["world", index: 6, …]
"abc123".replace(/\d+/, "###"); // Output: "abc###"
"a,b;c d".split(/[,;\s]+/); // Output: ["a", "b", "c", "d"]
Common Character Classes
| Token | Matches |
|---|---|
. |
any character except line terminators (use s flag for “any”) |
\d |
digit [0-9] |
\D |
non-digit |
\w |
word char [A-Za-z0-9_] |
\W |
non-word |
\s |
whitespace |
\S |
non-whitespace |
[abc] |
one of a, b, c |
[^abc] |
none of a, b, c |
[a-z] |
range |
\b |
word boundary (zero-width) |
^ / $ |
start / end (of input, or line with m flag) |
Quantifiers
| Token | Means |
|---|---|
* |
0 or more |
+ |
1 or more |
? |
0 or 1 |
{n} |
exactly n |
{n,} |
at least n |
{n,m} |
n to m |
*?, +?, ?? |
non-greedy (lazy) versions |
"<b>hi</b><i>x</i>".match(/<.*>/); // Output: ["<b>hi</b><i>x</i>"] greedy
"<b>hi</b><i>x</i>".match(/<.*?>/); // Output: ["<b>"] lazy
Flags
| Flag | Meaning |
|---|---|
g |
global — find all matches |
i |
case-insensitive |
m |
multiline — ^/$ match line boundaries |
s |
dotall — . matches newlines too |
u |
unicode — full code-point + \p{…} |
y |
sticky — match from lastIndex only |
d |
indices — .indices on each match (ES2022) |
const re = /foo/giu;
Tip: Almost always include u for text from users or the web — without it, surrogate-paired characters (emoji, many CJK chars) misbehave.
Capture Groups
const m = "2026-05-16".match(/^(\d{4})-(\d{2})-(\d{2})$/);
m[0]; // "2026-05-16" (whole match)
m[1]; m[2]; m[3]; // "2026" "05" "16"
Non-capturing group (?:…) when you need grouping but not the value:
"hello!!!".match(/(?:hello){2,}/); // matches "hellohello" if present
Named capture groups (much more readable):
const m = "2026-05-16".match(/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/);
m.groups.year; // "2026"
m.groups.month; // "05"
Backreferences
/(['"])(.*?)\1/.exec(`he said "yes"`);
// matches "yes" surrounded by whatever quote opened it
\1 (or \k<name> for named) refers to a previously captured group.
Lookaround (Zero-Width Assertions)
/\d+(?= USD)/.exec("100 USD"); // "100" — followed-by
/\d+(?! USD)/.exec("100 EUR"); // "100" — NOT followed-by
/(?<=\$)\d+/.exec("price $42"); // "42" — preceded-by
/(?<!\$)\d+/.exec("price 42"); // "42" — NOT preceded-by
Lookaround doesn’t consume characters — (?=foo) checks for foo ahead, but the cursor stays where it was. Great for pulling values out of a context without including the context in the match.
Anchors and Boundaries
/^\s*$/.test(""); // empty / whitespace only
/^https?:\/\//.test(url); // starts with http(s)://
/\.js$/.test("app.js"); // ends with .js
/\bcat\b/.test("category"); // false — word boundary
Unicode Property Escapes (with u)
/\p{Letter}+/u.test("naïve"); // true
/\p{Emoji}/u.test("hi 👋"); // true
/\p{Script=Greek}+/u.exec("αβγ"); // ["αβγ", …]
Massively more reliable than [A-Za-z] for real-world text.
The String Methods
| Method | What it does |
|---|---|
str.match(re) |
array of match(es); with g, all matches as strings |
str.matchAll(re) |
iterator of full match objects (needs g) |
str.replace(re, repl) |
first match; replace all if g |
str.replaceAll(re, repl) |
replace all; throws if regex lacks g |
str.search(re) |
index of first match, or -1 |
str.split(re) |
split into array |
for (const m of "a1 b2 c3".matchAll(/([a-z])(\d)/g)) {
console.log(m[1], m[2]); // a 1 / b 2 / c 3
}
Replacement with a Function
const out = "say hello to ada".replace(
/\b(\w)(\w*)/g,
(_, first, rest) => first.toUpperCase() + rest
);
// Output: "Say Hello To Ada"
The callback receives (match, p1, p2, …, offset, fullString, groups) — pure power.
Replacement Patterns
In a replacement string:
$&— the full match$1,$2— capture groups$<name>— named groups$\/$’` — text before / after the match
"John Smith".replace(/(\w+) (\w+)/, "$2 $1"); // "Smith John"
"2026-05-16".replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, "$<d>/$<m>/$<y>");
RegExp Methods
const re = /(\d+)/g;
re.exec("a1 b22 c333");
re.exec("a1 b22 c333");
// Each call advances re.lastIndex — call repeatedly to walk matches
exec + global flag is the low-level loop matchAll wraps for you.
Common Patterns
// Email-ish (good enough for UX, not RFC-perfect)
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// URL (scheme + host)
const URL_RX = /^https?:\/\/[^\s/]+/i;
// Hex colour
const HEX = /^#(?:[0-9a-f]{3}){1,2}$/i;
// Slug
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
// Strip HTML tags (naive — don't use on untrusted input)
"".replace(/<[^>]+>/g, "");
// Trim each line
text.replace(/^[ \t]+|[ \t]+$/gm, "");
// Camel → kebab
"helloWorldX".replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
// "hello-world-x"
Warning: Email/URL validation with regex is approximate at best. Use platform features (<input type="email">, new URL(str)) for real validation.
Performance and Catastrophic Backtracking
Some patterns explode on certain inputs:
/^(a+)+$/.test("aaaaaaaaaaaaaaaaaaaa!");
// Exponential — modern engines may hang/throw
Rules of thumb:
- Avoid nested quantifiers on overlapping classes (
(a+)+,(.*)*) - Anchor (
^,$) so the engine doesn’t try every offset - Prefer specific classes (
[^,]) over.to limit backtracking - For untrusted input, set a hard cap on input length
A Realistic Example — Markdown Link Extractor
const MD_LINK = /\[(?<text>[^\]]+)\]\((?<url>https?:\/\/[^\s)]+)\)/g;
function extractLinks(md) {
const out = [];
for (const m of md.matchAll(MD_LINK)) {
out.push({ text: m.groups.text, url: m.groups.url });
}
return out;
}
extractLinks(`See [docs](https://example.com/d) and [home](https://example.com).`);
// Output:
// [ { text: "docs", url: "https://example.com/d" },
// { text: "home", url: "https://example.com" } ]
Named groups + matchAll = readable extraction logic.
Common Mistakes
// ❌ Forgetting g with replaceAll
"aaa".replaceAll(/a/, "b"); // TypeError — needs /g
// ❌ Forgetting to escape special chars in dynamic patterns
new RegExp(userInput); // ReDoS risk + bad matches
// ✅ Escape helper
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
new RegExp(escape(userInput));
// ❌ Reusing /g regex without resetting lastIndex
const re = /x/g;
re.test("xy"); // true
re.test("xy"); // false ← lastIndex carried over
// ❌ Using . without u for emoji-containing strings
/.{1}/u.test("👋"); // true (without u: false)
// ❌ Thinking ^/$ are line-based by default
/^line$/.test("a\nline\nb"); // false; add m for multiline
Best Practices Checklist
- ✅ Prefer named groups
(?<name>…)for readability - ✅ Add the
uflag for any user-facing text - ✅ Use
matchAllfor iterating global matches - ✅ Use anchors and specific classes — avoid greedy
.*where possible - ✅ Escape user-supplied strings before injecting into regex
- ✅ Use platform validators (
URL,<input type="email">) before reaching for regex - ❌ Don’t rely on regex for parsing HTML or JSON
- ❌ Don’t reuse a global regex across calls without minding
lastIndex - ❌ Don’t write deeply nested quantifiers — beware catastrophic backtracking
What’s Next?
Text wrangling — solved. Next we look at two often-overlooked collection types that beat plain objects and arrays for many jobs: Sets and Maps (plus their weak cousins).