Regex Lookahead and Lookbehind — Zero-Width Assertions Explained
Regex lookaheads and lookbehinds match positions without consuming characters. Here's how positive and negative lookahead/lookbehind work, with practical examples for password...
Lookahead and lookbehind assertions let you match a position in the string based on what comes before or after — without including that surrounding text in the match. They’re “zero-width” because they don’t consume characters.
Use the Regex Tester to test regex patterns with lookahead and lookbehind.
Lookahead syntax
Positive lookahead (?=...)
Matches a position where the pattern ahead CAN be found:
\d+(?= dollars)
Matches digits that are followed by ” dollars”:
"100 dollars and 50 euros"
^^^ ← Only "100" matches (50 doesn't have " dollars" after)
The match is 100 — the ” dollars” text is checked but not included in the match.
Negative lookahead (?!...)
Matches a position where the pattern ahead CANNOT be found:
\d+(?! dollars)
Matches digits NOT followed by ” dollars”:
"100 dollars and 50 euros"
^^ ← Only "50" matches
Lookbehind syntax
Positive lookbehind (?<=...)
Matches a position where the pattern BEHIND was found:
(?<=\$)\d+
Matches digits preceded by ”$“:
"Price: $100 and €50"
^^^ ← Only "100" matches (50 preceded by €, not $)
Negative lookbehind (?<!...)
Matches a position where the pattern BEHIND was NOT found:
(?<!\$)\d+
Matches digits NOT preceded by ”$“:
"$100 and 50 items"
^^ ← Only "50" matches
Practical examples
Password validation
Validate a password contains at least one uppercase, one lowercase, one digit, and one special character:
const passwordRegex = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
// How it works:
// ^ — start of string
// (?=.*[A-Z]) — has at least one uppercase (anywhere in string)
// (?=.*[a-z]) — has at least one lowercase
// (?=.*\d) — has at least one digit
// (?=.*[@$!%*?&]) — has at least one special char
// [A-Za-z\d@$!%*?&]{8,} — all chars valid, min 8 length
// $ — end of string
passwordRegex.test('Passw0rd!'); // true
passwordRegex.test('password'); // false (no uppercase, no digit, no special)
passwordRegex.test('PASSWORD1!'); // false (no lowercase)
Extract price values
Extract the number from price strings:
const text = 'Regular: $29.99, Sale: $14.99, Tax: €2.50';
// Extract numbers preceded by $:
const prices = [...text.matchAll(/(?<=\$)\d+\.\d{2}/g)];
prices.map(m => m[0]); // ['29.99', '14.99']
// Extract numbers NOT preceded by $ (Euro prices):
const euroPrices = [...text.matchAll(/(?<!\$)\d+\.\d{2}/g)];
euroPrices.map(m => m[0]); // ['2.50']
Log parsing
Extract error messages after specific prefixes:
const logs = `
ERROR: Database connection failed
INFO: Server started on port 3000
WARN: Memory usage high
ERROR: Query timeout exceeded
`;
// Match only ERROR message content:
const errors = [...logs.matchAll(/(?<=ERROR: ).+/g)];
errors.map(m => m[0]);
// ['Database connection failed', 'Query timeout exceeded']
HTML tag content (simplified)
Extract text inside specific tags:
const html = '<h1>Main Title</h1><h2>Subtitle</h2><p>Content</p>';
// Extract h1 content:
const h1Match = html.match(/(?<=<h1>).*?(?=<\/h1>)/);
console.log(h1Match[0]); // 'Main Title'
Word boundary matching
Match a word only when not preceded or followed by a hyphen:
// Match "test" not inside "pre-test" or "test-case":
/(?<!-)\btest\b(?!-)/.test('Run the test'); // true
/(?<!-)\btest\b(?!-)/.test('Run the pre-test'); // false
/(?<!-)\btest\b(?!-)/.test('Run the test-case'); // false
Multiple lookaheads (chaining)
You can chain multiple lookaheads from the same position:
// Match a position where ALL conditions are true:
/(?=.*\d)(?=.*[a-z])(?=[A-Z]).{8,}/
// This works because all lookaheads check from the same position
// None of them advance the cursor
Lookahead in replace
Use lookahead in replace operations:
// Add comma as thousand separator:
const formatted = '1234567'.replace(/(?<=\d)(?=(\d{3})+(?!\d))/g, ',');
// '1,234,567'
// Replace only the first word of each sentence:
const text = 'Hello world. Hi there.';
const result = text.replace(/(?<=^|\. )[a-z]/gi, c => c.toUpperCase());
// Already uppercase, but shows the pattern
// Add prefix only to number not already prefixed:
const output = 'item-123 and 456'.replace(/(?<!item-)\d+/g, n => `item-${n}`);
// 'item-123 and item-456'
Browser support
| Feature | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
Positive lookahead (?=...) | All | All | All | All |
Negative lookahead (?!...) | All | All | All | All |
Positive lookbehind (?<=...) | 62+ | 78+ | 16.4+ | 8+ |
Negative lookbehind (?<!...) | 62+ | 78+ | 16.4+ | 8+ |
Lookbehind doesn’t work in older Safari. For Safari 15 and below, use alternative patterns or a polyfill.
Performance note
Nested lookaheads with .* can cause catastrophic backtracking on complex strings. Keep lookaheads simple and specific:
// RISKY: multiple .* in nested lookaheads on long strings
/(?=.*a)(?=.*b)(?=.*c).*/
// SAFER: use anchors and be specific
/^(?=[^a]*a)(?=[^b]*b)(?=[^c]*c)/
Related tools
- Regex Tester — test regex patterns online
- Regex Tester Online — regex testing guide
- Regex Patterns Cheatsheet — pattern reference
Related posts
- The 2026 Regex Cheatsheet (PCRE, JS, Python — Side by Side) — A dense reference for modern regex: anchors, character classes, quantifiers, loo…
- Catastrophic Backtracking: The Regex That Kills Your Server — One regex with nested quantifiers can reduce your server to 100% CPU in millisec…
- JavaScript Regex Flags — g, i, m, s, u, and v Explained — JavaScript regex flags change how patterns match. Learn when to use global /g, c…
- Regex Patterns — Ready-to-Use Patterns for Email, Phone, URL, and More — Ready-to-use regular expression patterns for validating emails, phone numbers, U…
- Regex Tester Online — Test Regular Expressions with Live Match Highlighting — A regex tester shows which parts of your test string match your pattern in real …
Related tool
Test regular expressions with live match highlighting and explanation.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.