Regex Lookahead and Lookbehind, Explained
What (?=), (?!), (?<=) and (?<!) actually do — zero-width assertions that check without consuming, with worked examples and JavaScript support notes.
Lookahead and lookbehind are assertions about context: (?=X) requires that X comes next, (?<=X) requires X came just before, and ?!/(?<! are the negative versions. They never consume characters — the match result contains only what the rest of the pattern matched, which is exactly why they exist: “match this, but only when it’s followed by that.”
The four forms
| Syntax | Name | Meaning | Example match |
|---|---|---|---|
X(?=Y) |
positive lookahead | X only if Y follows | \d+(?=px) → 640 in 640px |
X(?!Y) |
negative lookahead | X only if Y does not follow | \d+(?!px) → 640 in 640em |
(?<=Y)X |
positive lookbehind | X only if preceded by Y | (?<=\$)\d+ → 99 in $99 |
(?<!Y)X |
negative lookbehind | X only if not preceded by Y | (?<!\$)\d+ → 99 in €99 |
Try them live in the tester — explain mode spells out each assertion token in words.
The three jobs they’re hired for
1. Match without including the context. Extract the number before a unit, or the digits after a currency sign — (?<=\$)\d+(?= USD) matches 42 inside $42 USD and nothing else. Without lookarounds you’d capture the delimiters into the match and strip them afterwards.
2. Multi-condition validation. Lookaheads let one pattern enforce several independent rules because each one inspects the same position:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
Each (?=.*…) scans ahead from the start for one requirement — lowercase, uppercase, digit — and .{8,} finally consumes the string. This is the canonical password-rule pattern.
3. Zero-width insertion points. The classic thousands-separator trick works only because lookahead matches positions, not text: "1234567".replace(/\B(?=(\d{3})+(?!\d))/g, ",") → "1,234,567". \B finds every non-boundary spot where an exact multiple of three digits follows.
JavaScript specifics that bite
- Lookbehind needs ES2018+. All modern browsers and Node ≥10 have it; Safari lagged until 16.4 (2023). A bad lookbehind throws
SyntaxErroratnew RegExp()time — it kills the regex, not just the match. - Lookbehind must be fixed-length.
(?<=\$\d+)is a SyntaxError — the engine can’t tell how far back to look. Use a capture group (\$(\d+)) when the left context varies in length. - Alternatives inside lookbehind can differ in length —
(?<=cats|dogs)xis legal — but each branch is still fixed-width. - Zero-width ≠ zero risk. Nested quantifiers inside a lookahead still backtrack —
(?=(a+)+$)is a ReDoS shape even though it consumes nothing.
For the surrounding vocabulary — anchors, classes, lazy quantifiers — the regex cheat sheet has the full table.
Frequently asked questions
What does (?=...) do in a regex?
It's a positive lookahead: it asserts that the pattern inside matches starting at the current position, without consuming any characters. \d+(?=px) matches the digits in "640px" but not in "640em" — and the match itself is just "640", not "640px".
What does (?!...) do?
Negative lookahead — asserts the pattern does not match at the current position. foo(?!bar) matches the "foo" in "food" and "a foo walks in", but not in "foobar", where "bar" follows immediately. It's how you say "X not followed by Y".
Is lookbehind supported in JavaScript?
Yes, since ES2018 — every evergreen browser and Node ≥10. The holdout was Safari, which only added lookbehind in 16.4 (March 2023). If you support older Safari, new RegExp('(?<=x)y') throws a SyntaxError at construction — so the whole script's regexes die, not just the call. Feature-detect or use a capture group instead: /(?:x)(y)/ then read group 1.
Why does my lookahead pattern match an empty string?
Because lookarounds are zero-width — (?=a) alone matches a position, not text, so against "a" it succeeds at index 0 returning an empty match. A lookahead is only useful attached to something that consumes: \w+(?=@) consumes the name; (?=@) alone matches nothing but a spot in the string.
Can I use quantifiers inside a lookbehind in JavaScript?
Mostly no — JavaScript (like PCRE) requires lookbehind patterns to be fixed-length: (?<=\$\d+)x is rejected because \d+ makes the lookbehind's width unknown. Workaround: capture instead (\$(\d+) and read group 1), or use a variable-length-tolerant engine like .NET.