Developer Tools

What are lookaheads and lookbehinds (regex assertions)?

Master regex lookahead and lookbehind assertions to create complex pattern matching without consuming matched text.

By Inventive HQ Team

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.

Zero-width assertion: match a position without consuming characters The pattern \d+(?=px) applied to the string 100px. The digits 100 are consumed by the match while px is only looked at by the lookahead and left unconsumed. Assertion = check a position, consume nothing Pattern: \d+(?=px) applied to "100px" 1 0 0 p x \d+ consumed → match = "100" (?=px) looked at, not consumed cursor stays here — assertion returns it unchanged

The lookahead requires "px" to be there, but the match result is just the digits.

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):

SyntaxNameAsserts that...ExampleMatches
(?=...)Positive lookaheadthe text ahead does match\d+(?=px)"16" in 16px (not the px)
(?!...)Negative lookaheadthe text ahead does not match\d+(?!px)"16" in 16em; skips 16px
(?<=...)Positive lookbehindthe text behind does match(?<=\$)\d+"100" in $100 (not the $)
(?<!...)Negative lookbehindthe 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.

Advertisement

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]+>

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
EngineVariable-length lookbehind
JavaScript (ES2018+)Yes — unbounded ((?<=\d+) works)
.NETYes — unbounded
JavaBounded only — a finite max, e.g. (?<=\d{1,4})
Python re (built-in)No — fixed width only
PCRE / PHP / Perl / grep -PNo — 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.

Loading interactive tool...

Test them carefully:

  1. Test positive cases (should match)
  2. Test negative cases (should not match)
  3. Test edge cases (boundaries, special characters)
  4. 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.

Frequently Asked Questions

What is a lookahead in regex?

A lookahead is a zero-width assertion that checks whether text ahead of the current position matches a pattern, without adding that text to the match. Positive lookahead (?=...) asserts the following text does match; negative lookahead (?!...) asserts it does not. For example, \d+(?=px) matches the digits in "16px" but returns only "16" — the "px" is tested but never consumed, so the match cursor does not advance over it.

What is the difference between a lookahead and a lookbehind?

Direction. A lookahead (?=...) or (?!...) checks the text that comes after the current position; a lookbehind (?<=...) or (?<!...) checks the text that comes before it. Both are zero-width — they assert a condition is true at a position without capturing any characters. Lookahead is written after the thing it guards, lookbehind before it. So (?<=$)\d+ matches "100" in "$100" using a lookbehind, while \d+(?=px) matches "16" in "16px" using a lookahead.

What does zero-width assertion mean?

Zero-width means the assertion matches a position between characters rather than the characters themselves, so it consumes nothing and adds nothing to the match result. Anchors like ^ and $ are also zero-width assertions. Lookaheads and lookbehinds extend that idea: they test whether a whole sub-pattern is present ahead of or behind the current position, then hand the cursor back unchanged so the surrounding text stays available for the rest of the pattern to match.

Does JavaScript support lookbehind?

Yes. Lookbehind — both positive (?<=...) and negative (?<!...) — was added to JavaScript in ES2018 and is supported in all modern browsers, Node.js 9+, and Deno. JavaScript is also unusual in supporting variable-length lookbehind, so patterns like (?<=\w+\s)word work. Very old environments (Internet Explorer, Node 8 and earlier, some 2018-era Safari builds) lack lookbehind entirely, so test in your target runtime or transpile if you must support them.

What is the difference between positive and negative lookahead?

A positive lookahead (?=...) succeeds when the following text matches the pattern; a negative lookahead (?!...) succeeds when the following text does not match. \d+(?=px) matches numbers that are followed by "px"; \d+(?!px) matches numbers that are not followed by "px". The same positive/negative distinction applies to lookbehind: (?<=$)\d+ finds numbers after a dollar sign, (?<!$)\d+ finds numbers not after a dollar sign.

How do you use lookahead for password validation?

Chain independent positive lookaheads at the start of the pattern, each asserting one required character class, then match the length. ^(?=.[a-z])(?=.[A-Z])(?=.*\d).{8,}$ requires at least one lowercase letter, one uppercase letter, and one digit across a string of eight or more characters. Because each lookahead is zero-width, they all test from the same starting position without consuming anything, so their order does not matter and they combine as a logical AND.

Can you use variable-length lookbehind?

It depends on the engine. JavaScript and .NET allow variable-length (unbounded) lookbehind such as (?<=\d+)x. Java allows only bounded-length lookbehind (a finite maximum, e.g. (?<=\d{1,4})). Python's built-in re module and PCRE (used by PHP, Perl, and grep -P) require fixed-length lookbehind — Python's third-party regex module lifts that restriction. When a pattern must be portable, keep lookbehinds a fixed width like (?<=$) or (?<=\d\d).

What is the syntax for the four regex assertions?

Positive lookahead is (?=...), negative lookahead is (?!...), positive lookbehind is (?<=...), and negative lookbehind is (?<!...). Lookaheads go immediately after the position you are testing; lookbehinds go immediately before it. All four are zero-width, so they can be stacked and sequenced — for example (?<=$)\d+(?=.\d\d) matches whole-dollar digits sitting between a "$" behind and a ".NN" cents portion ahead.

regexassertionslookaheadlookbehindadvanced