How to Use Regex in JavaScript (With Real Examples)
A regular expression in JavaScript is a pattern object used to match character combinations in strings. You create one with /pattern/flags syntax or the RegExp constructor, then use methods like .test(), .match(), .replace(), and .matchAll() to search, extract, and transform text.
This post covers the JavaScript-specific regex API with working examples you can copy into your code right now. If you're brand new to regex syntax, start with our Regex for Beginners guide first. To test any of these patterns interactively, open the Regex Tester.
How do you create a regex in JavaScript?
JavaScript gives you two ways to create a regular expression, and they behave slightly differently.
Regex literal — the pattern sits between forward slashes:
const pattern = /\d{3}-\d{4}/g;
This gets compiled when the script loads. Use it when the pattern is fixed.
RegExp constructor — takes a string:
const pattern = new RegExp('\\d{3}-\\d{4}', 'g');
Notice the double backslashes. Because you're passing a string, the backslash needs its own escape. This is the version you want when the pattern comes from user input or a variable.
One gotcha: regex literals can't interpolate variables. If you need to match a dynamic value, you're stuck with new RegExp():
const username = 'joe';
const pattern = new RegExp(`@${username}\\b`, 'i');
'Hello @Joe!'.match(pattern); // ['@Joe']
Which regex method should you use: test, match, or matchAll?
Choosing the right method depends on what you need back. Here's the honest breakdown:
RegExp.prototype.test() returns a boolean. Use it when you just need yes or no:
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test('joe@example.com');
// true
Fast, simple, no allocation of match arrays. But there's a trap: if you use test() with the g flag, the regex remembers where it left off via .lastIndex. Calling test() again on the same regex object may return false because it's looking from a different position. Either drop the g flag or reset .lastIndex = 0 between calls.
String.prototype.match() returns an array of matches. Without the g flag, you get the first match plus captured groups. With g, you get all matches but lose group information:
// Without g — first match with groups
'2026-03-31'.match(/(\d{4})-(\d{2})-(\d{2})/);
// ['2026-03-31', '2026', '03', '31', index: 0, groups: undefined]
// With g — all matches, no groups
'foo123bar456'.match(/\d+/g);
// ['123', '456']
String.prototype.matchAll() (ES2020) is what you actually want for iterating over multiple matches with groups:
const text = 'price: $42.99, tax: $3.50';
const pattern = /\$(\d+\.\d{2})/g;
for (const match of text.matchAll(pattern)) {
console.log(`Full: ${match[0]}, Amount: ${match[1]}, Index: ${match.index}`);
}
// Full: $42.99, Amount: 42.99, Index: 7
// Full: $3.50, Amount: 3.50, Index: 21
matchAll() requires the g flag and returns an iterator — not an array. Spread it with [...text.matchAll(pattern)] if you need array methods.
How does replace work with regex in JavaScript?
String.prototype.replace() with a regex is one of the most useful combinations in the language. Without the g flag, it replaces only the first match. With g, it replaces all of them:
'foo-bar-baz'.replace(/-/g, '_');
// 'foo_bar_baz'
Since ES2021, String.prototype.replaceAll() exists, so you can skip the regex entirely for simple string replacements. But regex replace() still wins when you need pattern matching or backreferences.
Backreferences let you reuse captured groups in the replacement string:
// Swap first and last name
'Ladehoff, Joe'.replace(/(\w+), (\w+)/, '$2 $1');
// 'Joe Ladehoff'
// Wrap every word in brackets
'hello world'.replace(/(\w+)/g, '[$1]');
// '[hello] [world]'
Replacement functions give you full control. The function receives the match, groups, index, and original string:
'the QUICK brown FOX'.replace(/\b[A-Z]+\b/g, (word) => {
return word.toLowerCase();
});
// 'the quick brown fox'
This is where replace() gets powerful. Need to slugify a title? Convert temperature units? Transform date formats? A replacement function handles all of that.
// Convert "March 31, 2026" to "2026-03-31"
const months = { January: '01', February: '02', March: '03' /* ...etc */ };
'March 31, 2026'.replace(
/(\w+) (\d{1,2}), (\d{4})/,
(_, month, day, year) => `${year}-${months[month]}-${day.padStart(2, '0')}`
);
// '2026-03-31'
What are named capture groups and why should you use them?
Named capture groups (ES2018) let you label parts of your match with (?<name>...) instead of relying on numeric indices. This is a readability upgrade that matters in production code.
Compare these two approaches for parsing a URL:
// Numeric groups — what is $1 again?
const url = 'https://umbratools.dev/tools/regex-tester';
const match = url.match(/^(https?):\/\/([^/]+)(\/.*)?$/);
const protocol = match[1]; // 'https'
const host = match[2]; // 'umbratools.dev'
const path = match[3]; // '/tools/regex-tester'
// Named groups — self-documenting
const match2 = url.match(/^(?<protocol>https?):\/\/(?<host>[^/]+)(?<path>\/.*)?$/);
const { protocol, host, path } = match2.groups;
Named groups also work in replace() via $<name>:
'2026-03-31'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
);
// '03/31/2026'
In matchAll(), each result's .groups property is a plain object, which means you can destructure it directly:
const log = '2026-03-31 14:22 ERROR timeout | 2026-03-31 14:23 INFO retry';
const pattern = /(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}) (?<level>\w+) (?<msg>\w+)/g;
for (const { groups: { date, level, msg } } of log.matchAll(pattern)) {
console.log(`[${date}] ${level}: ${msg}`);
}
// [2026-03-31] ERROR: timeout
// [2026-03-31] INFO: retry
One thing to know: named groups are undefined (not absent) when they don't participate in the match. Check for that if you're using optional groups.
What JavaScript regex flags exist and when do you need them?
JavaScript supports nine regex flags as of ES2024. Here are the ones that actually come up:
g (global) — match all occurrences, not just the first. Required for matchAll(). Affects replace() behavior.
i (case-insensitive) — /hello/i matches "Hello", "HELLO", "hElLo". Straightforward.
m (multiline) — makes ^ and $ match line boundaries instead of string boundaries. You need this when processing multi-line text:
const csv = `name,age
joe,30
anna,28`;
csv.match(/^\w+/gm);
// ['name', 'joe', 'anna'] — matches start of each line
Without m, only 'name' would match because ^ anchors to the start of the entire string.
s (dotAll) — makes . match newline characters too. By default, . matches everything except \n. This catches people off guard when matching across lines:
'<div>\n content\n</div>'.match(/<div>.*<\/div>/s);
// ['<div>\n content\n</div>']
Without s, that match fails because . stops at the newline.
d (hasIndices, ES2022) — adds a .indices property to matches with start/end positions for each group. Useful for syntax highlighting or building source maps:
const match = 'foo123bar'.match(/(?<num>\d+)/d);
match.indices.groups.num; // [3, 6]
v (unicodeSets, ES2024) — the successor to the u flag. Enables set operations like subtraction and intersection within character classes, plus better Unicode property support:
// Match Greek letters but not archaic ones (intersection)
/[\p{Script=Greek}&&\p{Letter}]/v
The v flag is supported in Chrome 112+, Firefox 116+, Safari 17+, and Node.js 20+. If you're targeting modern environments, prefer v over u.
What are the most useful regex patterns for JavaScript?
Here are patterns I reach for constantly. All tested, all production-grade, all ready to copy.
Email validation (practical, not RFC 5322 compliant — that regex is 6,300 characters long and nobody uses it):
/^[^\s@]+@[^\s@]+\.[^\s@]+$/
This catches the obvious mistakes (missing @, spaces, missing domain) without false-rejecting valid but weird addresses. For real validation, send a confirmation email.
URL extraction from text:
const urls = text.match(/https?:\/\/[^\s)]+/g);
ISO date parsing:
const { groups } = '2026-03-31T14:30:00Z'.match(
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<min>\d{2}):(?<sec>\d{2})Z$/
);
Slug generation:
const slug = title
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '');
Strip HTML tags:
const text = html.replace(/<[^>]*>/g, '');
This is the quick-and-dirty version. For real HTML parsing, use DOMParser or a library like cheerio. Regex and HTML are a famously bad combination because HTML isn't a regular language — nested tags, attributes with > in them, comments, CDATA sections... the edge cases never end.
Extract query parameters:
const params = Object.fromEntries(
[...url.matchAll(/[?&](?<key>[^=&]+)=(?<value>[^&#]*)/g)]
.map(m => [m.groups.key, decodeURIComponent(m.groups.value)])
);
In practice, use new URL(url).searchParams instead. But if you're parsing URL-like strings that aren't actually valid URLs (log entries, partial strings), the regex version works where the URL constructor throws.
What performance pitfalls should you watch for?
Regex performance in JavaScript is usually fine until it isn't. Two situations cause real problems:
Catastrophic backtracking happens when a pattern with nested quantifiers hits a string that almost-but-doesn't-quite match. The engine tries every possible combination before giving up:
// DON'T: nested quantifiers on overlapping character classes
/^(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaa!');
// This can lock up your browser for seconds
The fix is to avoid nested quantifiers ((a+)+, (a*)*, (a+)*) on patterns where the inner and outer parts can match the same characters. Use atomic groups or possessive quantifiers if your engine supports them (JavaScript doesn't natively, but the v flag improves things).
Creating regex in loops is the other common mistake:
// Slow — compiles a new regex on every iteration
for (const line of lines) {
if (/^ERROR/i.test(line)) { /* ... */ }
}
// Better — compile once outside the loop
const errorPattern = /^ERROR/i;
for (const line of lines) {
if (errorPattern.test(line)) { /* ... */ }
}
V8 caches regex literals internally, so the performance difference here is small in modern engines. But with new RegExp(), the compilation cost is real and measurable in tight loops.
Quick reference
| Task | Method | Example |
|---|---|---|
| Yes/no match | regex.test(str) |
/\d/.test('abc3') → true |
| First match + groups | str.match(regex) |
'ab12'.match(/(\d+)/) → ['12', '12'] |
| All matches (no groups) | str.match(regex) with g |
'a1b2'.match(/\d/g) → ['1', '2'] |
| All matches + groups | str.matchAll(regex) |
[...'a1b2'.matchAll(/(\d)/g)] |
| Replace first | str.replace(regex, rep) |
'aab'.replace(/a/, 'x') → 'xab' |
| Replace all | str.replace(regex, rep) with g |
'aab'.replace(/a/g, 'x') → 'xxb' |
| Split on pattern | str.split(regex) |
'a, b;c'.split(/[,;]\s*/) → ['a', 'b', 'c'] |
| Named group | (?<name>...) |
match.groups.name |
| Case-insensitive | i flag |
/hello/i matches 'HELLO' |
| Multiline anchors | m flag |
/^line/gm matches each line start |
| Dot matches newline | s flag |
/<tag>.*<\/tag>/s |
Try any of these patterns in the Regex Tester to see matches highlighted in real time. If you're still building regex muscle memory, the beginner's guide covers the syntax fundamentals.
Try the tool mentioned in this article:
Open Tool →