Understanding Lookaheads and Lookbehinds
Lookaheads and lookbehinds are zero-width regex assertions: they check whether a pattern appears immediately ahead of or behind the current position without adding those characters to the match. A normal pattern consumes what it matches (the text becomes part of the result and the cursor moves past it); an assertion only peeks at a condition, then hands the cursor back untouched. That is why \d+(?=px) matches "16" in "16px" — the "px" is tested but never captured — and why you can stack several checks at one position, as in a password rule that must contain a lowercase letter and an uppercase letter and a digit.
That is the summary an AI overview will give you. What it can't show you is why "zero-width" changes how the whole engine behaves — that the match cursor never advances over the assertion — or the engine-by-engine reality of lookbehind support that turns a working pattern in JavaScript into a syntax error in Python. This guide covers both, with a diagram, a cheat-sheet table, and copy-pasteable examples for password validation and currency extraction.
Lookaheads and lookbehinds are advanced regex features that let you match patterns based on what comes before or after them, without including that surrounding text in the match. They are called "assertions" because they assert that a condition holds at the current position without consuming those characters. This enables sophisticated pattern-matching that would be impossible or extremely awkward using standard quantifiers and character classes alone.
The Four Assertions at a Glance
Every lookaround in regex is one of four combinations of direction (ahead vs. behind) and polarity (must match vs. must not match):
| Syntax | Name | Asserts that... | Example | Matches |
|---|---|---|---|---|
(?=...) | Positive lookahead | the text ahead does match | \d+(?=px) | "16" in 16px (not the px) |
(?!...) | Negative lookahead | the text ahead does not match | \d+(?!px) | "16" in 16em; skips 16px |
(?<=...) | Positive lookbehind | the text behind does match | (?<=\$)\d+ | "100" in $100 (not the $) |
(?<!...) | Negative lookbehind | the text behind does not match | (?<!\$)\d+ | "5" in Qty 5; skips $100 |
The mnemonic: the < in (?<= and (?<! points backward (lookbehind), and the ! always means not (negative). Everything else is a lookahead. Because all four are zero-width, you can stack and sequence them freely — (?<=\$)\d+(?=\.\d\d) grabs the whole-dollar digits sitting between a $ behind and a .NN cents portion ahead.
Lookahead Assertions
Lookahead asserts that something comes after the current position, without including it in the match.
Positive Lookahead (?=...)
Asserts that text matching the pattern exists ahead:
\d+(?=px)
Matches a number only if followed by "px":
- Matches in: "16px", "100px", "24px"
- Matches just: "16", "100", "24" (not the "px")
- Doesn't match: "16em", "24", "100pt"
Why Useful: Extract only the number, not the unit.
Negative Lookahead (?!...)
Asserts that text matching the pattern does NOT exist ahead:
\d+(?!px)
Matches a number only if NOT followed by "px":
- Matches in: "16em", "100pt", "24" (as standalone number)
- Doesn't match: "16px", "100px" (followed by px)
Why Useful: Exclude specific cases while matching.
Lookbehind Assertions
Lookbehind asserts that something comes before the current position, without including it in the match.
Positive Lookbehind (?<=...)
Asserts that text matching the pattern exists behind:
(?<=\$)\d+
Matches a number only if preceded by "$":
- Matches in: "$100", "Price: $50"
- Matches just: "100", "50" (not the "$")
- Doesn't match: "100", "€50"
Why Useful: Extract amounts preceded by currency symbol.
Negative Lookbehind (?<!...)
Asserts that text matching the pattern does NOT exist behind:
(?<!\$)\d+
Matches a number only if NOT preceded by "$":
- Matches in: "Item 100", "Quantity: 5"
- Doesn't match: "$100", "$50"
Why Useful: Find numbers not associated with currency.
Practical Examples
Password Validation
Require at least 8 characters with uppercase, lowercase, and digit:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
How It Works:
(?=.*[a-z])- Lookahead: assert lowercase exists(?=.*[A-Z])- Lookahead: assert uppercase exists(?=.*\d)- Lookahead: assert digit exists.{8,}- Actual match: 8+ characters
All assertions must be true for the string to match.
const password = "SecurePass123";
const pattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(pattern.test(password)); // true
Extract Numbers Not Preceded by $
(?<!\$)\d+
let text = "Price: $100, Quantity: 5, Cost: $20 each";
let matches = text.match(/(?<!\$)\d+/g);
// Result: ["5"] (only number not preceded by $)
Extract File Extensions (Last Part After Dot)
(?<=\.)\w+$
Matches file extension without the dot:
- In: "document.pdf", "image.png"
- Matches: "pdf", "png"
let filename = "photo.jpg";
let ext = filename.match(/(?<=\.)\w+$/)[0];
// Result: "jpg"
Match Closing Tag Without Opening
(?!</[a-zA-Z]+>)</?[a-zA-Z]+>
HTML Link Extraction
Extract URLs from href attributes without the protocol:
(?<=href=")(https?:)?//[^"]+(?=")
Find Lines Containing Specific Word
^(?=.*password).+$
Find any line containing "password":
/^(?=.*password).+$/m
Currency Amount Extraction
Extract prices in specific format:
\$(?=\d+\.\d{2})[\d.]+
Matches "$X.XX" format:
- Matches: "$100.00", "$5.99"
- Doesn't match: "$100", "$5.9"
Language Support for Lookahead and Lookbehind
JavaScript
Lookahead: Fully supported in modern browsers
/(?=test)/.test("test") // true
Lookbehind: Added in ES2018
/(?<=test)text/.test("testtext") // true
Older browsers don't support lookbehind.
Python
Both fully supported:
import re
re.search(r'(?=test)', 'test') # Lookahead
re.search(r'(?<=test)text', 'testtext') # Lookbehind
Java
Both supported:
Pattern.compile("(?=test)"); // Lookahead
Pattern.compile("(?<=test)text"); // Lookbehind
PHP
Both supported:
preg_match('/(?=test)/', 'test'); // Lookahead
preg_match('/(?<=test)/', 'test'); // Lookbehind
Important Limitations
Variable-Length Lookbehind
Most engines restrict what a lookbehind may contain, because the engine has to know how far back to look. A fixed-width lookbehind like (?<=\$) or (?<=\d\d) works almost everywhere; a variable-length one like (?<=\d+) does not:
(?<=\d+)test // variable length — rejected by many engines
(?<=[0-9])test // fixed length — supported everywhere
| Engine | Variable-length lookbehind |
|---|---|
| JavaScript (ES2018+) | Yes — unbounded ((?<=\d+) works) |
| .NET | Yes — unbounded |
| Java | Bounded only — a finite max, e.g. (?<=\d{1,4}) |
Python re (built-in) | No — fixed width only |
| PCRE / PHP / Perl / grep -P | No — fixed width only |
Python regex (third-party) | Yes |
Rule of thumb: if a pattern must be portable across languages, keep every lookbehind a fixed width. Only JavaScript and .NET can be relied on for truly unbounded lookbehind.
No Nested Lookahead/Lookbehind
INVALID: (?=(?<=test)) // Can't nest
VALID: (?<=test)(?=example) // Can sequence
Performance Considerations
Excessive lookahead/lookbehind can impact performance:
(?=.*a)(?=.*b)(?=.*c)(.*) // Works but slow
Better approach:
^(?=.*a)(?=.*b)(?=.*c).*$ // Better performance
Complex Examples
Find Words Not Preceded by "un"
(?<!un)\bword\b
Matches "word" but not "unword":
let text = "word is good, unword is bad";
let matches = text.match(/(?<!un)\bword\b/g);
// Result: ["word"]
Extract Hashtags from Twitter
(?<=#)\w+
Get hashtag content without the #:
let tweet = "This is #awesome #javascript code";
let tags = tweet.match(/(?<=#)\w+/g);
// Result: ["awesome", "javascript"]
Validate Number with Optional Decimals
\d+(?:\.\d+)?(?=[^.]*$)
Matches number but not if followed by extra dots.
Find Strings Between Tags
(?<=>).*?(?=<)
Matches content between > and <:
- In: "```>Hello
" - Matches: "Hello"
Debugging Assertions
Assertions can be tricky to debug. The fastest way to build one up is to paste it into a live tester and watch which characters get highlighted as you add each lookaround — you will immediately see that the assertion text is never part of the match.
Test them carefully:
- Test positive cases (should match)
- Test negative cases (should not match)
- Test edge cases (boundaries, special characters)
- Use looser versions first (remove assertions one at a time)
Debugging Approach:
// Start with basic pattern
const pattern1 = /\d+/; // Just numbers
// Add first lookahead
const pattern2 = /(?=px)\d+/; // Numbers before px
// Remove lookahead, add lookbehind
const pattern3 = /(?<=\$)\d+/; // Numbers after $
// Combine
const pattern4 = /(?<=\$)\d+(?=px)/ // Both conditions
Common Mistakes
Forgetting Direction
WRONG: (?=>) // Looks ahead for >
RIGHT: (?<=>) // Looks behind for >
Incorrect Assertion Type
WRONG: (?!=) // Negative lookahead, asserts NOT followed by =
RIGHT: (?=) // Positive lookahead, asserts followed by
Complex Nested Patterns
(?=.*complex.*pattern) // Can work but hard to understand
Better to break into separate assertions
Not Testing Cross-Platform
Lookbehind support varies. Always test in your target environment.
When to Use vs Alternatives
Use Lookahead/Lookbehind When:
- Need to match based on surrounding context
- Surrounding text shouldn't be included in match
- Improving readability (vs complex alternation)
Consider Alternatives When:
- Simple capture groups would work
- Performance is critical with complex assertions
- Language doesn't support (older JavaScript)
- Code is becoming too complex
Example: Rather than complex assertion, simple capture might work:
// Complex assertion:
(?<=\$)(\d+)
// Simple capture:
\$(\d+)
// Then use group 1 instead of full match
Conclusion
Lookaheads and lookbehinds are powerful regex features for context-sensitive pattern matching. Positive lookahead (?=) asserts future text without consuming it, while positive lookbehind (?<=) does the same for preceding text. Negative variants (?!and (?<!) assert text does NOT exist. These assertions enable sophisticated validation patterns (like password requirements) and precise data extraction. However, they can make patterns harder to understand and maintain, so use them judiciously. Always test assertions carefully across your target platforms, as support varies, particularly for lookbehind assertions in older JavaScript environments.