X Xerobit

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...

Mian Ali Khalid · · 5 min read
Use the tool
Regex Tester
Test regular expressions with live match highlighting and explanation.
Open Regex Tester →

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

FeatureChromeFirefoxSafariNode.js
Positive lookahead (?=...)AllAllAllAll
Negative lookahead (?!...)AllAllAllAll
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 posts

Related tool

Regex Tester

Test regular expressions with live match highlighting and explanation.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.