Quick answer
Lookaheads and lookbehinds are zero-width assertions: they check what is on either side of a position without consuming characters. Use them when you need to match something only in a specific context, such as a price preceded by a currency symbol or a tag not followed by a closing slash.
Lookaround is the feature that turns regex from "find this string" into "find this string in this context". Once you understand the four forms, a class of problems that needed two regexes or a callback becomes a single readable pattern.
// All four lookaround forms, by example
const text = 'Buy at $249.50 or 99 cents off MSRP $300';
// Positive lookahead: digits followed by " cents"
text.match(/\d+(?= cents)/); // ['99']
// Negative lookahead: digits NOT followed by " cents"
[...text.matchAll(/\d+(?! cents)/g)]
.map(m => m[0]); // ['249', '50', '300']
// Positive lookbehind: digits preceded by $
[...text.matchAll(/(?<=\$)\d+(?:\.\d{2})?/g)]
.map(m => m[0]); // ['249.50', '300']
// Negative lookbehind: digits NOT preceded by $
[...text.matchAll(/(?<!\$)\d+/g)]
.map(m => m[0]); // ['99'] (numbers without leading $)
What "zero-width" actually means
Most regex tokens consume characters: a matches and consumes one a. Anchors and lookarounds do not consume. They only check the position. After a successful match, the engine's cursor stays where it was before the assertion.
That is why you can chain assertions: (?=\d)(?=[02468]) matches a position where the next character is both a digit and even (i.e. an even digit). Each lookahead checks the same position from the same starting point.
Practical implication: lookarounds let you express "this character must exist in this context, and the match should be the character itself, not the surrounding context". Without them you would have to capture the surrounding context and discard it.
Positive lookahead: "must be followed by"
Syntax: (?=...). Match the position only if what follows matches the inner pattern. Examples:
\d+(?= USD)matches digits only when followed by the literal " USD".\b\w+(?=\()matches a word that is immediately followed by an opening parenthesis (handy for "find function names" in source).(?=.*[A-Z])(?=.*\d).{8,}matches strings of 8+ characters that contain at least one uppercase and at least one digit (a password rule).
The last pattern is the canonical reason developers reach for lookahead: chained assertions give you AND-style constraints inside a single regex.
Negative lookahead: "must NOT be followed by"
Syntax: (?!...). The position matches only if the inner pattern would not match starting there. Examples:
foo(?!bar)matches "foo" only when it is not followed by "bar". Matches "foobaz" and "foo!" but not "foobar".\b\d{3}(?![\d])matches exactly three digits not followed by another digit (so 123 but not 1234).https?://(?!localhost)\S+matches HTTP URLs that do not point to localhost (good for filtering external links).
Negative lookahead is also how you write "match X unless Y is right after it" without a second pass.
Lookbehind: "must be preceded by"
Syntax: (?<=...) for positive, (?<!...) for negative. Same idea as lookahead but checks what is before the current position.
(?<=\$)\d+matches digits that are immediately preceded by a dollar sign (without including the $ in the match).(?<!^)#\w+matches hashtags that are not at the start of the line (so word boundaries followed by # work, but Markdown headings do not).(?<=\bMr\.\s)\w+matches a name preceded by "Mr. ".
Lookbehind support: stable in V8 (Chrome, Node), Firefox, and Safari 16.4+. Python's built-in re requires fixed-width lookbehind; for variable-width, install the regex module. PCRE supports both.
Where lookaround quietly outperforms alternatives
Three classes of problems where lookaround is the right tool:
- Extracting in context. "All numbers preceded by $" is one regex with lookbehind, two passes without.
- Validation with multiple constraints. Password rules, structured-input checks (must contain letter AND digit AND symbol), all expressed as chained lookaheads.
- Splitting without losing the delimiter.
str.split(/(?<=\.)\s+/)splits a paragraph into sentences while keeping the period at the end of each sentence (because the lookbehind does not consume the period).
When to skip lookaround
Three signs you should reach for something else:
- You are nesting four lookaheads. The pattern is now write-only. Split into two regexes or move logic into the callback of
replace(). - You target an old runtime. Pre-Safari 16.4 lookbehind, pre-Python 3 word boundaries, both have edge cases. If you cannot drop legacy support, fall back to capture-and-discard.
- You need variable-width lookbehind in Python's built-in
re. The error "look-behind requires fixed-width pattern" is real. Install theregexmodule instead.
Otherwise, lookaround is the difference between regex that reads like a question and regex that reads like a hieroglyph.