Regex lookahead and lookbehind, explained with examples

Lookarounds let a pattern check what comes before or after a spot without including it in the match. Once that clicks, a lot of awkward regexes become one line.

The four lookarounds

SyntaxNameMeansExample
X(?=Y)Positive lookaheadX, if Y follows\d+(?=px) finds 12 in "12px"
X(?!Y)Negative lookaheadX, unless Y follows\d+(?!px|\d) skips "12px"
(?<=Y)XPositive lookbehindX, if Y comes before(?<=\$)\d+ finds 45 in "$45"
(?<!Y)XNegative lookbehindX, unless Y comes before(?<!-)\b\d+ skips negative numbers

"Zero-width": the idea that makes them click

Normal pattern parts consume characters: once px has matched, the engine has moved past it. A lookaround stands at one position, peeks, and reports yes or no without moving. That is why \d+(?=px) matches only the digits, and why several lookaheads can sit side by side and all check the same text from the same starting point.

Password rules in one pattern

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

Each lookahead starts at the beginning and scans ahead: "somewhere there is a digit", "a lowercase letter", "an uppercase letter". None of them consume anything, so .{8,} then checks the length of the whole string. Passw0rdx passes; password1 fails (no capital). For a real sign-up form, a clear checklist under the field is kinder than one big regex, and length matters more than character classes.

Thousands separators

"1234567".replace(/\B(?=(\d{3})+(?!\d))/g, ",")   // "1,234,567"

This finds every position inside the number (\B) that is followed by a multiple of three digits and then no more digits, and inserts a comma there. Nothing is matched, only positions, so no digits are lost. In real code, Intl.NumberFormat does this properly for every locale.

Exclude a word or ending

\b[\w.]+(?<!\.min)\.js\b      // app.js yes, app.min.js no
^(?!.*\bdraft\b).*$              // lines that do not contain the word draft

The second pattern is the standard way to say "does not contain" in a single regex, which plain character classes cannot do.

Which languages support them?

Try every pattern on this page in the regex tester: it highlights matches and groups live as you type, in your browser.

Questions

Why does my lookbehind give an invalid group error?

Either the engine does not support lookbehind (Go, Rust, very old Safari) or, in Python and PCRE, the lookbehind can match text of different lengths. Make it fixed-length or move the variable part outside.

Is a lookahead slower than a normal pattern?

Not by itself. Patterns like .* inside several lookaheads rescan the text, which is fine for short input such as a password but can be slow on large documents.

What is the difference between (?=...) and (?:...)?

(?:...) is a non-capturing group: it matches and consumes text, it just does not save it. (?=...) consumes nothing and only checks what comes next.

Published .

Try it

Related guides

Newest guides