Concept
A regular expression (regex) is a pattern that describes a set of strings. The JavaScript regex engine tests whether a string matches the pattern (test/exec) or finds matching substrings (match/matchAll/replace/split).
Regex syntax fundamentals
Literals /abc/ matches exactly "abc"
. any character except newline
\d digit [0-9]
\D non-digit
\w word character [a-zA-Z0-9_]
\W non-word character
\s whitespace (space, tab, newline, etc.)
\S non-whitespace
\b word boundary (position between \w and \W)
^ start of string (or line in multiline mode)
$ end of string (or line in multiline mode)Character classes
[abc] one of: a, b, or c
[^abc] anything except a, b, or c (negated class)
[a-z] any lowercase letter
[a-zA-Z0-9] any alphanumeric characterQuantifiers
? 0 or 1 (optional)
* 0 or more (greedy)
+ 1 or more (greedy)
{n} exactly n
{n,m} between n and m (inclusive)
{n,} n or more
Lazy quantifiers (match as few as possible):
*? +? ?? {n,m}?The difference between greedy and lazy matters when the pattern can match at multiple lengths:
const html = '<b>bold</b> and <i>italic</i>';
/<.+>/g.exec(html); // greedy: matches '<b>bold</b> and <i>italic</i>' (whole thing)
/<.+?>/g.exec(html); // lazy: matches '<b>' (smallest possible match)Groups and alternation
(abc) capturing group, captures the matched text
(?:abc) non-capturing group, groups without capturing
(?<name>abc) named capturing group
a|b alternation, matches a OR bconst date = '2024-12-25';
const match = date.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(match.groups.year); // '2024'
console.log(match.groups.month); // '12'
console.log(match.groups.day); // '25'Flags
g global: find all matches (not just first)
i case-insensitive
m multiline: ^ and $ match line start/end (not just string)
s dotAll: . matches \n too
u unicode: enables \u{XXXX} escapes and proper Unicode handling
d hasIndices: match result includes .indices (start/end positions)JavaScript regex API
// Test, returns boolean
/\d+/.test('abc123'); // true
// Exec, returns match array or null (stateful with /g flag!)
const re = /\d+/g;
let m;
while ((m = re.exec('a1 b2 c3')) !== null) {
console.log(m[0], m.index); // '1' 1, '2' 4, '3' 7
}
// String.match, returns all matches (with /g) or first match object
'a1 b2 c3'.match(/\d+/g); // ['1', '2', '3']
'a1 b2'.match(/(
Real-world patterns
// Email validation (simplified, full RFC 5322 is far more complex)
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// URL slugs
const slugRe = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
// Hex colors
const hexColorRe = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
// Phone (US, loose)
const phoneRe = /^\+?1
Lookahead and lookbehind
Lookahead asserts what follows without consuming characters:
// Positive lookahead: (?=...)
/\d+(?= dollars)/.exec('100 dollars'); // matches '100' (not ' dollars')
// Negative lookahead: (?!...)
/\d+(?! dollars)/.exec('100 euros'); // matches '100'Lookbehind asserts what precedes (ES2018):
// Positive lookbehind: (?<=...)
/(?<=\$)\d+/.exec('$100'); // matches '100' (not '$')
// Negative lookbehind: (?<!...)
/(?<!\$)\d+/.exec('100'); // matches '100'Common Mistakes
1. The stateful /g flag with RegExp.prototype.exec or test
A regex with /g maintains lastIndex. If you reuse the same regex object, test() alternates between true and false:
const re = /\d/g;
re.test('5'); // true (lastIndex = 1)
re.test('5'); // false (lastIndex reset to 0 after no match at pos 1)
re.test('5'); // true (lastIndex = 1 again)
// Fix: create a new regex each time, or use string methods2. Not escaping special characters in dynamic patterns
const userInput = 'a.b.c';
const re = new RegExp(userInput); // . means "any char", matches "aXbXc"
// Fix: escape special chars
const escaped = userInput.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const safeRe = new RegExp(escaped);3. Using regex for HTML parsing
Regex cannot handle recursive structures like HTML. Nested tags will defeat any regex. Use a DOM parser (DOMParser, cheerio, HTMLParser) for HTML.
4. Catastrophic backtracking (ReDoS)
Some patterns trigger exponential backtracking on certain inputs:
// Dangerous, (a+)+ on 'aaaaaaaaX' causes exponential backtracking
/^(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaX'); // hangs!
// Fix: use atomic groups or possessive quantifiers (not available in JS)
// Or restructure the pattern to avoid nested quantifiersThis is the "ReDoS" (Regular Expression Denial of Service) vulnerability.
5. Using match without the /g flag expecting all matches
'a1 b2 c3'.match(/\d+/); // ['1'], only first match!
'a1 b2 c3'.match(/\d+/g); // ['1', '2', '3'], all matchesBest Practices
- Test your regex with real data using regex101.com, paste your pattern, see exactly what matches and why (it explains each token).
- Use named capture groups (
(?<name>...)) for readability, especially when the pattern has many groups. - Comment complex patterns using verbose mode (not native to JS, use a wrapper library like
verbal-expressionsfor very complex cases, or split into multiple patterns with explanatory variables). - Be cautious with user input in
RegExpconstructor, always escape it to prevent ReDoS. - Prefer specific character classes over
.when you know what you're matching, it's faster and safer.
