Most engineers know just enough regex to write /^\d+$/ and then start copy-pasting from Stack Overflow the moment things get harder. That's fine for simple validation, but a handful of features — named groups, lookaround, sticky matching, and Unicode property escapes — cover the vast majority of real-world text extraction and validation tasks, and they're worth actually learning instead of reassembling from memory every time.
Named capture groups make regex maintainable
Numbered capture groups (match[1], match[2]) are unreadable six months later and break silently if someone reorders the pattern. Named groups fix both problems.
const logLine = "2026-02-08T14:32:01Z ERROR user_id=4821 failed login";
const pattern = /^(?<timestamp>\S+)\s+(?<level>\w+)\s+user_id=(?<userId>\d+)\s+(?<message>.+)$/;
const match = logLine.match(pattern);
if (match) {
const { timestamp, level, userId, message } = match.groups;
console.log(level, userId, message);
}This is the pattern you want for log parsing, structured extraction from semi-structured text, and anywhere the capture groups have obvious semantic meaning.
Lookahead and lookbehind for context without consuming it
Lookaround lets you assert that something precedes or follows a match without including it in the matched text. This is the tool for "match a number but only if it's followed by a currency symbol" or "match a word but only if it's not preceded by a specific prefix."
// Match digits only when immediately followed by "px"
const sizes = "width: 200px; margin: 10em; height: 50px".match(/\d+(?=px)/g);
// ["200", "50"]
// Negative lookbehind: match "error" not preceded by "no "
const text = "error: disk full — no error in cache layer";
const realErrors = text.match(/(?<!no )error/g);
// ["error"] — only the first oneLookbehind support is universal in current engines, but if you're writing a library that has to run in genuinely ancient environments, check your target runtime first — it was the last of the four lookaround forms to land everywhere.
The g/y flag and lastIndex gotcha
Using a regex with the global flag in a loop with .exec() mutates the regex object's lastIndex property between calls. This is a well-known footgun: reusing the same global regex literal across unrelated calls without resetting lastIndex produces matches that silently start from the wrong position.
const re = /\d+/g;
re.exec("abc123"); // matches "123", lastIndex now 6
re.exec("abc123"); // starts searching from index 6, finds nothing — returns null unexpectedlyThe fix is either to avoid reusing a stateful global regex across calls, or to explicitly reset re.lastIndex = 0 before each independent use. matchAll sidesteps this entirely — it returns an iterator and doesn't require you to manage state manually, so prefer it over manual exec loops when you just need every match.
Unicode property escapes for real-world text
\w and \d assume ASCII-ish input. If your app handles names, addresses, or free text from users worldwide, \p{L} (any letter in any script) and \p{N} (any number) with the u flag are what you actually want.
const isLetters = (s) => /^\p{L}+$/u.test(s);
isLetters("café"); // true
isLetters("北京"); // true
isLetters("hello1"); // falseWhen to stop using regex
Regex is a poor tool for anything with nesting or recursive structure — matching balanced parentheses, parsing JSON, or extracting values from HTML. It's not that it's impossible for simple cases, it's that the pattern becomes unreadable and fragile faster than you'd expect. The rule of thumb: if you find yourself writing a regex to validate or extract from a format that has its own grammar (JSON, HTML, a config DSL), stop and use an actual parser or the format's official library. Regex is for flat, line-oriented, or token-level matching — not for structure.