RegexLab

Lookahead & Lookbehind: Matching by Context

6 min readUpdated September 2026
Developer coding on a laptop

Sometimes you want to match something only because of what comes before or after it — a price only when it follows a dollar sign, a word only when it is not followed by a colon. Lookaround assertions solve this. They test a condition at a position but do not consume any characters, so the matched text stays exactly what you intended while the context is still checked.

Positive and negative lookahead

A lookahead looks to the right. Positive lookahead (?=...) succeeds if the pattern inside can match starting here; negative lookahead (?!...) succeeds if it cannot. To match a number only when it is followed by px, write \d+(?=px): against 16px the match is just 16, with px confirmed but left out of the result. To match a word not followed by a colon, use \w+(?!:).

Because lookahead does not consume characters, you can stack several at one position to require multiple conditions at once. That is the classic way to validate a password: ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$ demands at least one digit, one lowercase and one uppercase letter, then eight or more characters overall — each lookahead scanning the same string independently.

Lookbehind

A lookbehind looks to the left. Positive lookbehind (?<=...) requires the preceding text to match; negative lookbehind (?<!...) requires that it does not. To capture the amount after a currency symbol without including the symbol, write (?<=\$)\d+(?:\.\d{2})?; against $19.99 the match is 19.99. To match a number that is not preceded by a minus sign, use (?<!-)\b\d+.

Lookbehind arrived later than lookahead and support varies: JavaScript has supported it since ES2018, and most modern engines do too, but some older environments do not. When a pattern must run everywhere, confirm lookbehind is available or rewrite it with a capture group instead.

When to reach for lookaround

Reach for lookaround when the context matters but should not be part of the match — trimming a prefix or suffix, enforcing several rules at once, or splitting on a boundary you must keep. If instead you want to extract the surrounding text, a capture group is usually simpler and more portable. A good habit is to reach for the plainest tool that works and only add lookaround when it genuinely earns its place.

Lookaround patterns are easy to get subtly wrong, so test them against both matching and non-matching examples. Paste a handful of lines into the tester, including the edge cases you are trying to exclude, and confirm the highlight lands exactly where you expect before trusting the pattern.

Support, and the fixed-length question

Lookahead has been in JavaScript since the beginning; lookbehind arrived only in ES2018 and took years to reach every browser — Safari shipped it in 16.4, in March 2023. If your code runs in a build pipeline that targets older engines, a lookbehind will throw a `SyntaxError` at regex-construction time rather than fail quietly, so it is worth a feature test when the environment is not fully under your control.

JavaScript is also unusually generous about what a lookbehind may contain. Most engines — .NET being the other notable exception — require the lookbehind to be of fixed length, so `(?<=\$\d+)` is rejected. V8, SpiderMonkey and JavaScriptCore all allow variable-length lookbehind and match it right to left. That is convenient, but it means a pattern that works in Node may be rejected when the same expression is moved into Python, Java or Go.

Zero width has consequences

Lookaround consumes nothing, and that is exactly why it is useful for splitting. `"fooBarBaz".split(/(?=[A-Z])/)` yields `["foo", "Bar", "Baz"]` because the split points are positions rather than characters, so nothing is thrown away. The same trick inserts thousands separators: `replace(/\B(?=(\d{3})+(?!\d))/g, " ")` marks every position that has a multiple of three digits ahead of it, without touching the digits themselves.

The flip side is that a pattern made only of lookarounds matches an empty string, and with the `g` flag an empty match forces the engine to advance `lastIndex` by hand. Most built-in methods do this for you, but a hand-written `while (re.exec(s))` loop over such a pattern will spin forever. The rule of thumb: every iteration must consume at least one character, so pair lookaround with something that actually matches.

The takeaway

Lookaround checks context without consuming it: (?=) and (?!) look ahead, (?<=) and (?<!) look behind. Use it to trim prefixes and suffixes or enforce several rules at once, and always test the edge cases you mean to exclude.

Try it live
Open the regex tester

Try this pattern live with match highlighting.