Skip to main content

How to Use Lookahead and Lookbehind in JavaScript Regular Expressions

Most regex patterns consume characters as they match. They move through the string position by position, eating up text as they go. But sometimes you need to match something only if it is followed by or preceded by a certain pattern, without including that surrounding context in the match itself. This is exactly what lookahead and lookbehind assertions do.

These zero-width assertions let you peek forward or backward in the string, check a condition, and then decide whether the current match should succeed, all without consuming any characters. They are the secret behind elegant solutions for password validation, number formatting, conditional matching, and complex text parsing.

In this guide, you will learn all four types of lookaround assertions, understand how they work internally, and see real-world patterns that would be difficult or impossible without them.

What Are Lookaround Assertions?

A lookaround is a special regex construct that checks whether a pattern exists at a certain position in the string but does not consume any characters. The regex engine "looks" ahead or behind, verifies the condition, and then continues matching from the same position.

There are four types:

TypeSyntaxMeaning
Positive LookaheadX(?=Y)Match X only if followed by Y
Negative LookaheadX(?!Y)Match X only if not followed by Y
Positive Lookbehind(?<=Y)XMatch X only if preceded by Y
Negative Lookbehind(?<!Y)XMatch X only if not preceded by Y

The key concept: lookarounds are zero-width. They assert a condition at a position but do not include the checked text in the match result.

Positive Lookahead: X(?=Y)

A positive lookahead matches X only when it is immediately followed by Y. The Y part is checked but not included in the match.

Basic Syntax and Behavior

let pattern = /\d+(?= dollars)/g;

let text = "I have 100 dollars and 50 euros and 200 dollars.";
console.log(text.match(pattern));

Output:

[ '100', '200' ]

The pattern \d+(?= dollars) matches one or more digits only if they are followed by " dollars". Notice that "50" is not matched because it is followed by " euros", not " dollars". Also notice that the matched result is just the digits, not the word "dollars." The lookahead checked for it but did not consume it.

How It Works Step by Step

Let's trace how the engine processes /\d+(?= dollars)/ against "100 dollars":

  1. The engine matches \d+, consuming "100"
  2. It reaches the lookahead (?= dollars)
  3. It peeks ahead from the current position and checks: is " dollars" next?
  4. Yes, it is. The lookahead succeeds
  5. The match result is "100" (the lookahead text is not included)

If the lookahead had failed:

  1. The engine matches \d+, consuming "50"
  2. It reaches (?= dollars) and peeks ahead
  3. The next text is " euros", not " dollars"
  4. The lookahead fails
  5. The entire match attempt at this position fails

Lookahead Does Not Move the Position

This is the most important concept. After a lookahead check, the engine's position stays where it was before the lookahead. This means other patterns can still match the same text:

let pattern = /\d+(?= dollars) dollars/g;
let text = "100 dollars";

console.log(text.match(pattern));

Output:

[ '100 dollars' ]

The lookahead checked for " dollars", confirmed it exists, and then the literal " dollars" in the pattern matched and consumed those characters.

Multiple Lookaheads at the Same Position

You can chain multiple lookaheads to enforce several conditions simultaneously at the same position:

// Match a position followed by both a digit and then a letter somewhere
let pattern = /(?=.*\d)(?=.*[a-z])/;

console.log(pattern.test("abc123")); // true (has digit AND letter)
console.log(pattern.test("123456")); // false (no letter)
console.log(pattern.test("abcdef")); // false (no digit)
Output:

Output:

true
false
false

Each lookahead starts checking from the same position because neither one consumes any characters. This chaining technique is the foundation of password validation patterns.

Negative Lookahead: X(?!Y)

A negative lookahead matches X only when it is not followed by Y. It is the inverse of the positive lookahead.

Basic Syntax

let pattern = /\d+(?! dollars)/g;

let text = "100 dollars and 50 euros and 200 dollars";
console.log(text.match(pattern));

Output:

[ '10', '50', '20' ]

Wait, why "10" and "20" instead of nothing? This is a subtle but important behavior. Let's break it down:

  • At "100": the engine tries \d+ and greedily matches "100". Then it checks (?! dollars) and finds " dollars" follows. The lookahead fails. But the engine backtracks: it tries matching "10" instead. After "10", the next character is "0", not " dollars". The lookahead succeeds. So "10" matches.
  • The same logic produces "20" from "200".

To correctly match whole numbers not followed by "dollars," add a word boundary or more context:

let pattern = /\b\d+\b(?! dollars)/g;

let text = "100 dollars and 50 euros and 200 dollars";
console.log(text.match(pattern));

Output:

[ '50' ]

Now \b\d+\b matches complete numbers, and (?! dollars) ensures they are not followed by " dollars".

caution

Negative lookaheads with greedy quantifiers can produce unexpected partial matches due to backtracking. Always consider whether you need word boundaries or other anchors to match complete tokens.

Matching Words Not Followed by Something

// Match "java" only when NOT followed by "script"
let pattern = /java(?!script)/gi;

let text = "I know Java, JavaScript, and JavaBeans.";
console.log(text.match(pattern));

Output:

[ 'Java', 'Java' ]

The first "Java" (standalone) matches because it is followed by ",", not "script". The "Java" in "JavaScript" is skipped. The "Java" in "JavaBeans" matches because it is followed by "Beans".

Excluding Specific Patterns

// Match all <div> tags except self-closing ones
let pattern = /<div(?!\s*\/>)[^>]*>/g;

let html = '<div class="a"> <div/> <div class="b"> <div />';
console.log(html.match(pattern));

Output:

[ '<div class="a">', '<div class="b">' ]

The negative lookahead (?!\s*\/>) excludes <div/> and <div /> patterns.

Positive Lookbehind: (?<=Y)X

A positive lookbehind matches X only when it is immediately preceded by Y. The lookbehind checks what comes before the current position.

Basic Syntax

let pattern = /(?<=\$)\d+(\.\d{2})?/g;

let text = "Prices: $100, €50, $29.99, £75, $0.50";
console.log(text.match(pattern));

Output:

[ '100', '29.99', '0.50' ]

The pattern (?<=\$)\d+ matches digits only when preceded by a $ sign. The $ is checked but not included in the match. Only the number is captured.

Extracting Values After Labels

let pattern = /(?<=temperature:\s*)\d+/gi;

let data = "Temperature: 72, Humidity: 45, Temperature: 68";
console.log(data.match(pattern));

Output:

[ '72', '68' ]

The lookbehind (?<=temperature:\s*) ensures we only capture numbers that follow "temperature:" with optional whitespace.

Lookbehind with Fixed and Variable Length

In most regex engines, lookbehinds must have a fixed length. JavaScript (V8 engine) is more flexible and allows variable-length lookbehinds:

// Variable-length lookbehind - works in modern JavaScript
let pattern = /(?<=\$\s*)\d+/g;

let text = "$100 and $ 200 and $ 300";
console.log(text.match(pattern));

Output:

[ '100', '200', '300' ]

The \s* inside the lookbehind matches zero or more spaces, making the lookbehind variable-length. This works in modern JavaScript engines but may not be supported in older environments.

note

Variable-length lookbehinds work in Chrome, Edge, Node.js (V8), and Firefox. Safari added support in version 16.4+. If you need broad compatibility, stick to fixed-length lookbehinds or test thoroughly.

Negative Lookbehind: (?<!Y)X

A negative lookbehind matches X only when it is not preceded by Y.

Basic Syntax

let pattern = /(?<!\$)\b\d+\b/g;

let text = "$100 and 200 and $50 and 75";
console.log(text.match(pattern));

Output:

[ '200', '75' ]

The pattern matches numbers that are not preceded by a dollar sign. $100 and $50 are excluded, while 200 and 75 match.

Matching Words Not Preceded by Something

// Match "Smith" only when NOT preceded by "Mr. "
let pattern = /(?<!Mr\.\s)Smith/g;

let text = "Mr. Smith met Jane Smith and Dr. Smith at the park.";
console.log(text.match(pattern));

Output:

[ 'Smith', 'Smith' ]

The "Smith" after "Mr." is excluded. The ones after "Jane" and "Dr." are matched.

Preventing Matches Inside Larger Words

// Match "port" but not when part of "import", "export", "transport"
let pattern = /(?<!im|ex|trans)port/g;

let text = "import export transport port airport";
console.log(text.match(pattern));

Output:

[ 'port', 'port' ]

Here, "port" inside "import", "export", and "transport" is excluded. The standalone "port" and "port" inside "airport" match (since "air" is not "im", "ex", or "trans").

Combining Lookaheads and Lookbehinds

You can use lookaheads and lookbehinds together in the same pattern for precise matching.

Extracting Content Between Delimiters

// Match text between quotes, without including the quotes
let pattern = /(?<=")[^"]+(?=")/g;

let text = 'He said "hello" and she said "goodbye" to everyone.';
console.log(text.match(pattern));

Output:

['hello', ' and she said ', 'goodbye']

The lookbehind (?<=") checks for an opening quote, [^"]+ matches the content, and the lookahead (?=") checks for the closing quote. Neither quote is included in the match.

Matching Between HTML Tags

// Extract content between <b> tags without including the tags
let pattern = /(?<=<b>).*?(?=<\/b>)/g;

let html = "<p>This is <b>bold</b> and <b>important</b> text.</p>";
console.log(html.match(pattern));

Output:

[ 'bold', 'important' ]

Matching a Word Surrounded by Specific Context

// Match numbers that are between parentheses
let pattern = /(?<=\()\d+(?=\))/g;

let text = "Call (555) 123-4567 or (800) 999-0000. Reference: 42";
console.log(text.match(pattern));

Output:

[ '555', '800' ]

The number 42 is not matched because it is not inside parentheses.

Use Case: Password Validation

Password validation is the classic use case for multiple lookaheads. You can enforce several requirements simultaneously by chaining positive and negative lookaheads at the start of the string.

Basic Password Requirements

// Password must:
// - Be at least 8 characters long
// - Contain at least one uppercase letter
// - Contain at least one lowercase letter
// - Contain at least one digit

let strongPassword = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/;

console.log(strongPassword.test("MyPass1234")); // true
console.log(strongPassword.test("mypass1234")); // false (no uppercase)
console.log(strongPassword.test("MYPASS1234")); // false (no lowercase)
console.log(strongPassword.test("MyPassword")); // false (no digit)
console.log(strongPassword.test("MyP1")); // false (too short)

Output:

true
false
false
false
false

Let's break down how this works:

PartWhat It Does
^Anchored to start of string
(?=.*[A-Z])Look ahead: at least one uppercase letter somewhere
(?=.*[a-z])Look ahead: at least one lowercase letter somewhere
(?=.*\d)Look ahead: at least one digit somewhere
.{8,}Actually match 8 or more characters
$Anchored to end of string

All three lookaheads start checking from the same position (the start of the string). Each one scans through the entire string with .* looking for its required character. If all three succeed, the engine then proceeds to match .{8,}$.

Adding Special Character Requirement

let strongPassword = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;

console.log(strongPassword.test("MyPass12!")); // true
console.log(strongPassword.test("MyPass1234")); // false (no special char)

Output:

true
false

Adding a Negative Lookahead for Exclusions

// Password must NOT contain spaces
let password = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?!.*\s).{8,}$/;

console.log(password.test("MyPass12")); // true
console.log(password.test("My Pass 12")); // false (contains spaces)

Output:

true
false

A Complete Password Validator Function

function validatePassword(password) {
let checks = {
length: /.{8,}/.test(password),
uppercase: /(?=.*[A-Z])/.test(password),
lowercase: /(?=.*[a-z])/.test(password),
digit: /(?=.*\d)/.test(password),
special: /(?=.*[!@#$%^&*()_+\-=\[\]{}|;:,.<>?])/.test(password),
noSpaces: /^(?!.*\s)/.test(password)
};

let isValid = Object.values(checks).every(Boolean);

return { isValid, checks };
}

console.log(validatePassword("MyP@ss99"));
console.log(validatePassword("weak"));

Output:

{
isValid: true,
checks: { length: true, uppercase: true, lowercase: true, digit: true, special: true, noSpaces: true }
}
{
isValid: false,
checks: { length: false, uppercase: false, lowercase: true, digit: false, special: false, noSpaces: true }
}

Use Case: Number Formatting with Thousand Separators

This is one of the most elegant uses of lookaround assertions. Adding commas to large numbers:

// Add commas as thousand separators: 1234567 → 1,234,567
let formatted = "1234567890".replace(
/\B(?=(\d{3})+(?!\d))/g,
","
);

console.log(formatted);

Output:

1,234,567,890

This pattern is dense but powerful. Let's break it down:

PartMeaning
\BA non-word boundary (not at the start of the number)
(?=Positive lookahead: look ahead for...
(\d{3})+One or more groups of exactly three digits
(?!\d)Negative lookahead: NOT followed by another digit
)End of outer lookahead

The pattern finds positions within the number where the remaining digits form groups of three. At each such position, it inserts a comma.

You can wrap this in a reusable function:

function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

console.log(formatNumber(1000)); // "1,000"
console.log(formatNumber(1000000)); // "1,000,000"
console.log(formatNumber(42)); // "42"
console.log(formatNumber(9999999999)); // "9,999,999,999"

Output:

1,000
1,000,000
42
9,999,999,999
tip

For production code, Number.prototype.toLocaleString() or the Intl.NumberFormat API handles number formatting with locale awareness. The regex approach is excellent for learning lookarounds and works well for simple formatting needs.

Use Case: Complex Text Parsing

Extracting Currency Amounts Without the Symbol

// Get amounts that follow any currency symbol
let pattern = /(?<=[$€£¥])\d+(?:\.\d{2})?/g;

let text = "Prices: $29.99, €45.00, £12.50, ¥1000, and 500 units.";
console.log(text.match(pattern));

Output:

[ '29.99', '45.00', '12.50', '1000' ]

The 500 is excluded because it is not preceded by a currency symbol.

Splitting on Delimiters Without Removing Them

Normally, split() removes the delimiter. Lookarounds let you split while keeping the delimiters:

// Split before uppercase letters (camelCase → words)
let camel = "backgroundColor";
let words = camel.split(/(?=[A-Z])/);

console.log(words);

Output:

[ 'background', 'Color' ]

The lookahead (?=[A-Z]) finds positions before uppercase letters without consuming them, so the uppercase letter stays with the second part.

// Convert camelCase to kebab-case
function camelToKebab(str) {
return str
.replace(/(?<=[a-z])(?=[A-Z])/g, "-")
.toLowerCase();
}

console.log(camelToKebab("backgroundColor")); // "background-color"
console.log(camelToKebab("fontSize")); // "font-size"
console.log(camelToKebab("borderTopLeftRadius")); // "border-top-left-radius"

Output:

background-color
font-size
border-top-left-radius

The pattern (?<=[a-z])(?=[A-Z]) matches the zero-width position between a lowercase and an uppercase letter. It inserts a hyphen at that position.

Parsing Key-Value Pairs with Context

// Extract values only when the key is "color" or "size"
let pattern = /(?<=(?:color|size):\s*)\S+/g;

let css = "color: red; font-size: 14px; size: large; weight: bold; color: blue;";
console.log(css.match(pattern));

Output:

[ 'red;', 'large;', 'blue;' ]

Matching Whole Words Not Inside Quotes

// Match "TODO" only when not inside quotes
// This is simplified (real string parsing needs a proper parser)
let pattern = /(?<!")TODO(?!")/g;

let code = `
// TODO: fix this
let msg = "TODO: do not match this";
// TODO: also fix this
`;

console.log(code.match(pattern));

Output:

[ 'TODO', 'TODO' ]
warning

Using lookarounds to detect whether text is "inside quotes" works only for simple cases. Proper string-aware parsing requires a state machine or tokenizer, not regex alone.

Lookaround with Capturing Groups

Lookaround assertions can contain capturing groups. The groups capture text even though the lookaround itself does not consume characters:

// Capture the currency symbol in a lookbehind
let pattern = /(?<=(?<currency>[$€£]))\d+(?:\.\d{2})?/g;

let text = "$100 and €250.50 and £75";
let match;

while ((match = pattern.exec(text)) !== null) {
console.log(`Amount: ${match[0]}, Currency: ${match.groups.currency}`);
}

Output:

Amount: 100, Currency: $
Amount: 250.50, Currency: €
Amount: 75, Currency: £

The lookbehind checks for a currency symbol and captures it in a named group, even though the symbol is not part of the main match.

Lookaround vs. Consuming Alternatives

Understanding when to use a lookaround versus a regular consuming pattern:

// WITHOUT lookahead ("USD" is consumed, included in the match)
let consuming = /\d+ USD/g;
let text = "100 USD and 200 EUR";
console.log(text.match(consuming));
// Output: [ '100 USD' ]

// WITH lookahead ("USD" is checked but NOT in the match)
let lookahead = /\d+(?= USD)/g;
console.log(text.match(lookahead));
// Output: [ '100' ]

Use lookarounds when:

  • You need the context for matching but do not want it in the result
  • You need to check multiple conditions at the same position (chained lookaheads)
  • You want to insert text at specific positions using replace()
  • You need to split at positions without losing delimiter characters

Use regular consuming patterns when:

  • You want the context included in the match
  • The pattern is simpler without lookarounds
  • Performance is critical and the pattern is in a hot loop

Common Mistakes

Mistake 1: Confusing Lookahead Direction

// WRONG: lookbehind syntax where lookahead is needed
// "Match digits followed by px"
let wrong = /(?<=px)\d+/g; // This checks what comes BEFORE, not after

// CORRECT
let right = /\d+(?=px)/g;

let text = "width: 100px; height: 200px;";
console.log(text.match(right));

Output:

[ '100', '200' ]

Remember:

  • Lookahead (?=...) checks what comes after (to the right)
  • Lookbehind (?<=...) checks what comes before (to the left)

Mistake 2: Expecting Lookarounds to Consume Characters

// Expecting the match to include the lookahead content
let pattern = /\d+(?=\s*dollars)/g;

let text = "I have 100 dollars";
let match = text.match(pattern);
console.log(match[0]); // "100" (not "100 dollars"!)

Output:

100

If you want "100 dollars" in the result, do not use a lookahead. Just write the full pattern:

let pattern = /\d+\s*dollars/g;
console.log("I have 100 dollars".match(pattern));

Output:

[ '100 dollars' ]

Mistake 3: Greedy Quantifiers Causing Partial Matches with Negative Lookahead

// Intention: match numbers NOT followed by "px"
// WRONG: greedy \d+ backtracks to find a match
let bad = /\d+(?!px)/g;

let text = "100px 200em 300px";
console.log(text.match(bad));

Output:

[ '10', '200', '30' ]

The engine matches "100" but the negative lookahead sees "px" and fails. It backtracks to "10" where the next character is "0", not "px". So "10" matches. This is almost never what you want.

The fix depends on your intent:

// Match complete numbers not followed by "px"
let good = /\b\d+\b(?!px)/g;

let text = "100px 200em 300px";
console.log(text.match(good));

Output:

[ '200' ]

Word boundaries \b force the engine to match the complete number, preventing partial backtracked matches.

Mistake 4: Using Lookbehind with Unsupported Variable Length

While modern JavaScript engines support variable-length lookbehinds, some older environments do not:

// May not work in older browsers
let pattern = /(?<=\w+\s+)\d+/g;

// Fixed-length alternative that works everywhere
let safe = /(?<=\w\s)\d+/g;

If you need to support older environments, restructure the pattern to avoid variable-length lookbehinds or use a capturing group approach instead:

// Instead of lookbehind, capture what you need
let pattern = /\w+\s+(\d+)/g;
let text = "price 100 quantity 50";
let match;

while ((match = pattern.exec(text)) !== null) {
console.log(match[1]); // The number part only
}

Output:

100
50

Performance Considerations

Lookarounds generally add a small performance cost because the engine must check additional conditions at each position. A few guidelines:

  • Simple lookarounds like (?=\d) or (?<=\$) are very fast
  • Lookarounds with .* like (?=.*[A-Z]) scan the rest of the string, which is slower on long inputs
  • Multiple chained lookaheads (as in password validation) each scan independently, multiplying the work
  • Nested quantifiers inside lookarounds can cause excessive backtracking

For performance-critical code, consider whether a non-regex approach might be simpler:

// Regex password validation (scans string multiple times)
let regex = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;

// Imperative approach (single pass through the string)
function validatePassword(pw) {
if (pw.length < 8) return false;
let hasUpper = false, hasLower = false, hasDigit = false, hasSpecial = false;
for (let ch of pw) {
if (ch >= 'A' && ch <= 'Z') hasUpper = true;
else if (ch >= 'a' && ch <= 'z') hasLower = true;
else if (ch >= '0' && ch <= '9') hasDigit = true;
else hasSpecial = true;
}
return hasUpper && hasLower && hasDigit && hasSpecial;
}

Both approaches are valid. For typical password lengths (under 100 characters), the regex version is perfectly fine. For scanning large text buffers, the imperative approach may be faster.

Quick Reference Table

AssertionSyntaxReads AsExampleMatches
Positive LookaheadX(?=Y)X followed by Y/\d+(?=px)/"12" in "12px"
Negative LookaheadX(?!Y)X NOT followed by Y/\d+(?!px)/"12" in "12em"
Positive Lookbehind(?<=Y)XX preceded by Y/(?<=\$)\d+/"50" in "$50"
Negative Lookbehind(?<!Y)XX NOT preceded by Y/(?<!\$)\d+/"50" in "€50"

Key properties shared by all four:

  • Zero-width: they do not consume characters
  • Assert a condition: they pass or fail without affecting the match length
  • Can contain groups: capturing inside lookarounds works
  • Can be chained: multiple lookarounds at the same position enforce multiple conditions

Summary

Key takeaways:

  • Positive lookahead (?=Y) matches a position followed by Y without consuming it
  • Negative lookahead (?!Y) matches a position not followed by Y
  • Positive lookbehind (?<=Y) matches a position preceded by Y without consuming it
  • Negative lookbehind (?<!Y) matches a position not preceded by Y
  • Lookarounds are zero-width assertions: they check conditions but do not include the checked text in the match
  • Multiple lookaheads can be chained at the same position to enforce several conditions, which is the basis of password validation patterns
  • Lookarounds enable powerful operations like inserting text at specific positions, splitting without removing delimiters, and extracting content between delimiters
  • Watch out for backtracking with negative lookaheads: greedy quantifiers can produce surprising partial matches. Use word boundaries or anchors to match complete tokens
  • Modern JavaScript engines support variable-length lookbehinds, but older environments may not

Lookaround assertions transform regex from a simple pattern-matching tool into a context-aware engine. Once you internalize the four types, patterns that seemed impossible become straightforward.