Regex lookahead and lookbehind: a practical guide with examples
regex javascript developer-tools
Regex lookahead and lookbehind: a practical guide with examples
Lookahead and lookbehind are the most powerful — and most misunderstood — features of regular expressions. They let you assert that a pattern does or does not match at a given position, without consuming characters. Once you internalize them, a whole class of problems that seem to require multiple passes become a single regex.
This guide covers positive and negative lookahead, positive and negative lookbehind, the JavaScript-specific quirks (variable-width lookbehind landed in V8 only in 2018), and the three mistakes that bite everyone.
Want to test these patterns live in your browser without installing anything? Use the free Regex Tester — supports all JS regex flags, capture groups, and live highlighting.
The core idea: zero-width assertions
A normal regex token like a matches the letter a and advances the cursor by one character. A lookahead or lookbehind is a zero-width assertion: it checks whether a pattern matches at the current position, but does not advance the cursor.
Think of it as asking a question: "Is there a digit ahead of me?" — yes or no. The cursor stays put.
| Construct | Meaning | Example | Matches |
(?=X) | Positive lookahead | a(?=b) | a only if followed by b |
(?!X) | Negative lookahead | a(?!b) | a only if NOT followed by b |
(?<=X) | Positive lookbehind | (?<=a)b | b only if preceded by a |
(? | Negative lookbehind | (? | b only if NOT preceded by a |
The X inside can be any regex pattern, including groups and alternation.
Positive lookahead (?=X)
Match a word only if it is followed by another word
\w+(?=,\s*\d)
Test string: apple, 5 banana, orange, 3
Matches: apple (followed by , 5), orange (followed by , 3). Does not match banana because it is followed by , orange.
The classic: validate a password has at least one digit
^(?=.*\d).+$
Breakdown:
^— start of string(?=.*\d)— assert that somewhere ahead there is a digit.+— match the whole string (only reached if the assertion passed)$— end of string
^(?=.\d)(?=.[A-Z])(?=.[!@#$%^&]).{8,}$
Each lookahead starts from the beginning (because .* resets position) and asserts one rule. This is the idiomatic way to express "all of these conditions" in a single regex.
Negative lookahead (?!X)
Match a word that is not followed by a specific word
\b\w+\b(?!,\s*\d)
Test string: apple, 5 banana, orange, 3
Matches: banana. Does not match apple or orange because each is followed by a comma and a digit.
The "not followed by" trick for filtering
A common request: "Match foo in a string, but not when it is part of foobar."
foo(?!bar)
Test string: foo foobar foo baz
Matches: the first foo and the third foo. Skips the foo inside foobar.
Positive lookbehind (?<=X)
Match a price only if preceded by a currency symbol
(?<=\$)\d+(?:\.\d+)?
Test string: Price: $42.50 or €30 or $9
Matches: 42.50, 9. Skips 30 because it is preceded by €, not $.
Extract text after a label
(?<=Email:\s*)\S+@\S+
Test string: Name: Alice\nEmail: alice@example.com\nPhone: 555-1234
Matches: alice@example.com. The Email:\s* is asserted, not consumed, so it does not appear in the match.
Negative lookbehind (?
Match a word that is not preceded by a specific character
(?<!\$)\b\d+\b
Test string: I have $5 and 10 apples
Matches: 10. Skips 5 because it is preceded by $.
Avoid matching inside a comment
A classic code-search problem: "Find TODO in code, but not inside a // comment."
(?<!//.*)TODO
Note: this specific pattern has issues with multi-line comments and is simplified for illustration. The real-world version often needs a more sophisticated approach.
Variable-width lookbehind: the JavaScript story
Lookbehind was added to JavaScript in ECMAScript 2018. Initially, V8 (Chrome, Node.js) only supported fixed-width lookbehind — meaning every alternative inside (?<=...) had to match a fixed number of characters.
(?<=ab|abc)X // fixed-width: both alternatives are not the same length
This was rejected. You had to pad alternatives:
(?<=ab|(?:a)bc)X // still rejected, the alternatives must have equal length
The workaround was ugly. Fortunately, V8 6.2+ (Chrome 62+, Node.js 10+) shipped variable-width lookbehind, and as of 2026 every modern browser supports it. The fixed-width restriction is history.
If you need to support very old browsers (IE, old Safari), avoid lookbehind entirely and use a capture group instead:
// Instead of (?<=\$)\d+
const match = text.match(/\$(\d+)/);
if (match) console.log(match[1]);
The three mistakes everyone makes
Mistake 1: Forgetting lookahead does not consume
a(?=b)c
This never matches anything. Why? a(?=b) matches a only if the next char is b, but does not consume the b. Then c requires the next char to be c — but we just asserted it is b. Contradiction.
Fix: if you want to consume the b, write abc or a(?=b)bc.
Mistake 2: Trying to capture inside a lookbehind
(?<=(\w+):)\d+
You might expect match[1] to be the word before the colon. It does work in some engines, but the capture is not always what you think — lookbehind is an assertion, not a match. Safer:
\w+:(\d+)
Capture the digit, not the label.
Mistake 3: Anchor confusion
^(?=\d).+$
This asserts "string starts with a digit" and then matches the whole string. But the ^ inside the lookahead is redundant — lookahead starts from the current position, and ^ only matches at position 0. The pattern works, but the ^ is noise.
Cleaner:
(?=\d).+$
Or, if you really want the start anchor:
^(?=\d).+
Real-world examples
Extract all hashtags from a tweet
(?<=\s|^)#\w+
Matches #javascript, #regex, etc., without consuming the leading space.
Validate an email-like string has a TLD of at least 2 chars
^[^@\s]+@[^@\s]+\.(?=[a-z]{2,}$)[a-z]+$
The lookahead asserts the TLD is 2+ lowercase letters and reaches end of string.
Find function calls that are not preceded by new
(?<!new\s+)MyClass\(
Matches MyClass( when it is a function call, not an instantiation.
Split a camelCase string into words
(?<=[a-z])(?=[A-Z])
This is a zero-width split — it matches the position between a lowercase and an uppercase letter. Use it with String.split:
'camelCaseString'.split(/(?<=[a-z])(?=[A-Z])/);
// ['camel', 'Case', 'String']
Cheatsheet
(?=X) positive lookahead "assert X follows"
(?!X) negative lookahead "assert X does NOT follow"
(?<=X) positive lookbehind "assert X precedes"
(?<!X) negative lookbehind "assert X does NOT precede"
Key rules:
- Zero-width — the cursor does not move.
- Can contain any pattern, including groups and alternation.
- Variable-width lookbehind is supported in all modern browsers (2026).
- Captures inside lookarounds work but are often confusing — prefer explicit captures.
Try it yourself
Want to test these patterns without setting up a sandbox? Open the free RuMystic Regex Tester:
- Supports all JS regex flags (
g,i,m,s,u,y). - Shows capture groups and named groups.
- Live highlighting of matches.
- No upload — your test strings stay in your browser.