Lookahead and lookbehind

Assert what must come before or after a match without including it, and build validation patterns that stay readable.

Lookahead

A lookahead is a zero-width assertion about what follows the current position. Because it consumes nothing, the inspected text is still available for the rest of the pattern and for replacements.

// positive: must be followed by
/\d+(?=px)/.exec('12px 40em')[0];        // '12', the px is not consumed

// negative: must not be followed by
/\d+(?!px)/.exec('12px 40em')[0];       // '40'

// several lookaheads at one position behave as an AND
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
strong.test('Passw0rd');                 // true
strong.test('password1');                // false, no uppercase

// a capture inside a lookahead is still recorded
/(?=(\d{3}))(\d+)/.exec('12345').slice(1);   // [ '123', '12345' ]

// add a comma to a number without consuming the digits
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',');   // '1,234,567'
  • Lookarounds are zero-width, so consecutive lookaheads all inspect the same suffix. That is what makes the password AND-chain work.
  • Lookahead is available in every engine in common use: JavaScript, Python, Java, PCRE, .NET and RE2.
  • A lookahead is not a substitute for consuming input. If you need the text in the result, capture it instead of asserting on it.

Lookbehind and its limits

import re

re.search(r'(?<=\$)\d+', 'price $42').group()          # '42'
re.search(r'(?<!\$)\d+', 'price $42, qty 3').group()   # '3'

# fixed width only: every alternative must have the same length
re.search(r'(?<=cat|dog)nap', 'a catnap')               # fine, both are three characters
re.search(r'(?<=cat|cats )nap', 'the cats nap')         # raises re.error: widths differ
EngineLookbehind support
JavaScript (ES2018 and later)yes, variable length
Python 3yes, but the width must be fixed
Javayes, bounded and unbounded forms
PCRE2yes, variable length
.NETyes, variable length
RE2 and Gono lookbehind at all

Fixed width means every alternative must match the same number of characters: (?<=cat|dog) is fine while (?<=cat|dogs) raises an error in Python. Rewrite that as two patterns, or capture the prefix and strip it afterwards.

Overlapping matches

Normal matching resumes after the end of the previous match, so /aa/g finds two occurrences in a four-letter run. When you need overlapping results you have to advance the search position by one character yourself.

function overlapping(text, re) {
  const out = [];
  let i = 0;
  while (i <= text.length - 1) {
    re.lastIndex = i;
    const m = re.exec(text);
    if (!m) break;
    out.push(m[0]);
    i = m.index + 1;            // NOT m.index + m[0].length
  }
  return out;
}

overlapping('aaaa', /aa/g);     // [ 'aa', 'aa', 'aa' ]
overlapping('banana', /ana/g);  // [ 'ana', 'ana' ]
⚠️
Lookarounds are convenient but not free: repeating one inside a quantifier can backtrack badly, and some engines cap lookbehind length. For a password rule, prefer a few separate test calls over one pattern holding five lookaheads — it is easier to read and far easier to debug.

FAQ

When should I use a lookaround instead of a capture group?
Use a lookaround when you need to assert context but must not include it in the match — for example, extracting a number that follows a currency symbol while keeping the symbol out of the result. Use a capture group when you actually want the text back.
Why does my lookbehind throw in Python but work in JavaScript?
JavaScript and PCRE allow variable-length lookbehind, Python requires a fixed width. Rewrite the pattern so each alternative of the lookbehind is the same length, or match the prefix and remove it in code.

Groups, captures and backreferences Flags and matching modes

Last refreshed 2026-09-18.