Regular Expressions
Regular expressions match and transform text using patterns built into JS - write them as /foo/ literals or new RegExp("foo"), with flags like g (global) and i (case-insensitive).
A regular expression is a small pattern language for matching text. JavaScript has it built in: you can write a pattern as a literal between slashes (/foo/) or construct one at runtime with new RegExp("foo"). Methods on strings and RegExp objects let you test, search, capture, and replace using those patterns.
test checks whether a pattern appears anywhere in a string and returns a boolean. match returns the matched text (and capture groups) or null. The g flag makes match return every occurrence instead of just the first.
// test - returns true or false
console.log(/cat/.test("concatenate")); // true
console.log(/cat/.test("dog")); // false
// match without g - first occurrence + index
const result = "hello world".match(/\w+/);
console.log(result[0]); // "hello"
console.log(result.index); // 0
// match with g - all occurrences as an array
const all = "hello world".match(/\w+/g);
console.log(all); // ["hello", "world"]Capture groups, written as (...), let you extract specific parts of a match. exec returns a single match with its groups as array slots. Named groups ((?<name>...)) make the captured values accessible by name instead of by index, which keeps regex code readable when there are several groups.
// Numbered capture groups
const numbered = /(\w+)@(\w+)/.exec("user@example");
console.log(numbered[1]); // "user"
console.log(numbered[2]); // "example"
// Named capture groups - much easier to read
const named = /(?<user>\w+)@(?<host>\w+)/.exec("user@example");
console.log(named.groups.user); // "user"
console.log(named.groups.host); // "example"
// matchAll iterates all matches including their groups
const emails = "a@b.com c@d.com";
for (const match of emails.matchAll(/(?<user>\w+)@(?<host>[\w.]+)/g)) {
console.log(match.groups.user, match.groups.host);
}
// "a" "b.com"
// "c" "d.com"replace swaps matched text with a string or the return value of a function. Passing a function replacer is the most flexible form: it receives the full match plus any capture groups, so you can transform each match individually.
// Simple string replacement (g flag replaces all)
const dashes = "2026-04-27".replace(/-/g, "/");
console.log(dashes); // "2026/04/27"
// Function replacer - called once per match
const words = "hello world";
const titled = words.replace(/\b\w/g, (char) => char.toUpperCase());
console.log(titled); // "Hello World"
// Function replacer with named groups
const date = "2026-04-27";
const reordered = date.replace(
/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
(_, y, m, d) => `${d}/${m}/${y}`
);
console.log(reordered); // "27/04/2026"In production
Catastrophic backtracking - where the regex engine exhausts every possible combination before giving up - can hang the event loop on user-supplied input. The classic trap is nested quantifiers like (a+)+ applied to a string that nearly matches: the engine tries exponentially many paths. Anchor your patterns (^ and $) where you can, keep quantifiers flat, and run any regex that touches untrusted strings through a static analyzer like safe-regex or the eslint-plugin-security rule detect-unsafe-regex.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free