Regular Expressions in JavaScript
debt(d6/e3/b3/t8)
Closest to 'specialist tool catches it' (d5), but slightly worse. ESLint and Semgrep can catch obvious patterns (missing /u flag, g flag in loops), but ReDoS vulnerabilities and subtle Unicode mismatches often slip through automated detection without specialized regex analysis tools. Runtime testing is frequently needed to surface stateful lastIndex bugs. Score d6 reflects that common cases are caught by linters, but many regex-specific hazards require careful manual review.
Closest to 'simple parameterised fix' (e3). The quick_fix guidance (add /u flag, use named capture groups, test patterns) is straightforward within a single regex or small group of regexes. Fixing test() with g flag loops or match() without g is a one-file refactor. However, fixing widespread ReDoS vulnerabilities or Unicode handling across multiple regexes may require more care, keeping it solidly at e3 rather than e1.
Closest to 'localised tax' (b3). Regex choices are typically contained within individual functions or string-matching utilities. They do not impose system-wide architectural constraints (unlike custom auth or shared mutable state). However, if a codebase adopts a convention around regex flags or named capture groups, that becomes a light productivity tax on all future pattern authors. Score b3 reflects that the burden is local to regex-heavy components, not cross-cutting.
Closest to 'serious trap' (t7), verging on t8. The canonical misconception (JS regex behaves like PCRE, and g flag test() is stateless) directly contradicts how similar concepts work in other languages. The stateful lastIndex behaviour is non-obvious and causes alternating true/false in loops—a famous gotcha. Unicode handling without /u silently fails on non-ASCII input. These are not one-off edge cases but systematic misunderstandings that catch competent developers regularly. Score t8 reflects that the obvious way (test() in a loop, no flags) is often wrong, and the trap is deeply rooted in language design.
Also Known As
TL;DR
Explanation
JS regex: /pattern/flags or new RegExp(pattern, flags). Key methods: test() (returns boolean), match() (returns array or null), matchAll() (returns iterator of all matches — requires g flag), replace()/replaceAll() (string replacement), split() (split by pattern). Flags: g (global — all matches), i (case-insensitive), m (multiline — ^ and $ match line boundaries), s (dotAll — . matches newlines), u (Unicode — enables \u{1F600} and correct Unicode handling), d (indices — adds match index info). Named capture groups: (?<name>pattern). Lookahead: (?=) positive, (?!) negative.
Common Misconception
Why It Matters
Common Mistakes
- Using RegExp.test() with g flag in a loop — lastIndex advances causing false negatives.
- Not using the u flag for Unicode strings — \w does not match accented characters without u.
- match() without g flag returns only first match — use matchAll() for all matches.
- Not anchoring patterns — /cat/ matches concatenate, not just the word cat.
Code Examples
// Stateful g flag bug — alternating results:
const re = /hello/g; // g flag makes lastIndex stateful
console.log(re.test('hello world')); // true (lastIndex=5)
console.log(re.test('hello world')); // false (lastIndex=0 after miss)
console.log(re.test('hello world')); // true (lastIndex=5 again)
// match() only returns first:
'aaa'.match(/a/); // ['a'] — not all three!
// Avoid g flag with test() — or reset lastIndex:
const hasHello = /hello/.test('hello world'); // No g — safe
// matchAll for all matches:
const matches = [...'aaa'.matchAll(/a/g)]; // [{...}, {...}, {...}]
// Named capture groups:
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
const { groups: { year, month, day } } = '2026-03-16'.match(dateRe);