Lookahead and lookbehind

Regular expressions usually match a run of characters and hand them back to you. But sometimes the thing you want isn’t defined by what it is so much as by what sits next to it. You want the number that comes right before a currency sign, or the word that is not preceded by a @. The characters around your target matter, yet you don’t want them in the result.

That’s the job of lookahead and lookbehind. Together they’re called lookaround, and they let you attach a condition — “only if followed by this” or “only if preceded by that” — without pulling those neighboring characters into the match.

To ground the idea, take the string 3 candles cost 40€. Say you want the price: a number that is immediately followed by a sign. The 3 should be ignored; only 40 qualifies.

3 candles cost 40↑ matched↑ tested, not consumed
The price is the digit run that sits right before €. The € itself is a condition, not part of what we extract.

Lookahead

The syntax is pattern:X(?=Y). Read it as: “find pattern:X, but keep it only if pattern:Y comes right after.” Both pattern:X and pattern:Y can be any pattern you like.

For an integer that is followed by subject:€, the expression is pattern:\d+(?=€):

let str = "3 candles cost 40€";

alert( str.match(/\d+(?=€)/) ); // 40
// the number 3 is ignored — nothing (well, a space) follows it, not €

The key detail: the lookahead pattern:(?=...) is a test, not a capture. The subject:€ never lands in the result. The match is the bare string match:40.

Edit the pattern below and watch which parts of the string light up. Try removing the (?=€) to see 3 get matched too, or change to \s to hunt for numbers followed by a space instead:

interactiveLive lookahead tester

Here’s how the engine thinks about pattern:X(?=Y). It locates a candidate pattern:X, then peeks at the position right after it and asks, “is there a pattern:Y here?” If yes, pattern:X is accepted. If no, the engine throws that candidate away and keeps scanning. The peek never moves the read position forward — that’s why the assertion is often described as “zero-width.”

1. match \d+ → “40”2. peek: is next char ?yes → keep “40”
The read cursor stays put after phase 2 — the € stays in the string, out of the result.
Lookahead evaluates in two phases at the same spot: consume X, then look ahead at Y without advancing.

You can chain more than one test at the same position. pattern:X(?=Y)(?=Z) reads as:

  1. Find pattern:X.
  2. Check that pattern:Y follows right after pattern:X. Skip if it doesn’t.
  3. Check that pattern:Z also follows right after pattern:X. Skip if it doesn’t.
  4. If both pass, pattern:X matches. Otherwise, keep searching.

So you’re demanding that pattern:X be followed by pattern:Y and pattern:Z at once, both measured from the same starting point. That only works when pattern:Y and pattern:Z can be true simultaneously — they can’t be mutually exclusive.

For example, pattern:\d+(?=\s)(?=.*40) looks for a digit run that is followed by a space (pattern:(?=\s)) and that has 40 somewhere later in the string (pattern:(?=.*40)):

let str = "3 candles cost 40€";

alert( str.match(/\d+(?=\s)(?=.*40)/) ); // 3

In this string, 3 fits both tests: a space sits right after it, and 40 appears further along. 40 itself fails the first test, since follows it, not a space.

Negative lookahead

Now flip the goal. From the same kind of string, you want the quantity, not the price — a number that is not followed by subject:€.

That’s negative lookahead. The syntax is pattern:X(?!Y): “match pattern:X, but only if pattern:Y does not come right after it.”

let str = "5 candles cost 80€";

alert( str.match(/\d+\b(?!€)/g) ); // 5  (the price 80 is not matched)

The pattern:\b there is a word boundary. Without it, pattern:\d+ would happily match 8 inside 80 — since 0 follows 8, not , the negative test passes and you’d get a wrong partial match. The boundary forces the digit run to end at a real edge, so 80 is tested as a whole and correctly rejected because sits right after it.

X(?=Y) positive
keep X only when Y is next
\d+(?=€) → 40
X(?!Y) negative
keep X only when Y is not next
\d+\b(?!€) → 5
Positive vs negative lookahead: same X, opposite verdicts on what follows.

Lookbehind

Lookahead sets a condition on what comes after. Lookbehind is the mirror image: it sets a condition on what comes before. It matches a pattern only when the right thing precedes it.

  • Positive lookbehindpattern:(?<=Y)X matches pattern:X, but only if pattern:Y is directly before it.
  • Negative lookbehindpattern:(?<!Y)X matches pattern:X, but only if pattern:Y is not directly before it.

Notice the assertion sits ahead of pattern:X in the pattern, since “before” is to the left.

Switch the currency to US dollars, where the sign leads the number. To grab the amount in $40, use pattern:(?<=\$)\d+ — digits preceded by subject:$:

let str = "3 candles cost $40";

// the dollar sign is escaped as \$ because $ is a special regexp character
alert( str.match(/(?<=\$)\d+/) ); // 40  (the lone 3 is skipped)

And for the quantity — a number not preceded by subject:$ — reach for negative lookbehind pattern:(?<!\$)\d+:

let str = "5 candles cost $80";

alert( str.match(/(?<!\$)\b\d+/g) ); // 5  (the price 80 is not matched)

Here the assertion reads leftward. Start with (?<=\$)\d+ to grab prices, then flip it to (?<!\$)\b\d+ to grab the quantities instead — the same string, opposite verdict on what precedes each number:

interactiveLive lookbehind tester
$40(?<=$)\d+ — test the $ on the left, keep 40
40\d+(?=€) — test the € on the right, keep 40
Lookbehind reads to the left of X; lookahead reads to the right. Neither is included in the result.

Capturing groups

By default the text inside a lookaround stays out of the result. In pattern:\d+(?=€), the subject:€ isn’t captured — that’s the whole point, you’re matching a number and merely asserting a follows.

But sometimes you do want to keep part of the lookaround. You can, by wrapping that part in its own capturing group pattern:(...). The group’s text then shows up as an extra entry in the match array, even though the assertion itself still doesn’t advance the main cursor.

Here the currency sign pattern:(€|kr) is captured alongside the amount:

let str = "3 candles cost 40€";
let regexp = /\d+(?=(€|kr))/; // extra parentheses around €|kr

alert( str.match(regexp) ); // 40, €

The result array holds match:40 at index 0 (the full match) and match:€ at index 1 (group 1). The full match is still just 40 — the rides along only because you asked for it in a group.

The same works for lookbehind:

let str = "3 candles cost $40";
let regexp = /(?<=(\$|£))\d+/;

alert( str.match(regexp) ); // 40, $

See the array split apart below. The pattern starts as \d+(?=(€|kr)) — index [0] holds the consumed 40, while index [1] holds the grabbed by the group inside the lookahead. Drop the inner parentheses to \d+(?=€|kr) and [1] disappears, proving the group is what surfaces the character, not the assertion:

interactiveWhat the match array holds
match[0] — full match
“40”
match[1] — group inside lookaround
“€”
A capturing group inside a lookaround adds a value to the result array, but the full match [0] is still only the consumed part.

Doing it without lookaround

Lookaround is convenient, but it isn’t magic — for simple cases you can reach the same result by hand: match broadly, then filter by context in a loop.

The tools for that are str.match (without the pattern:g flag) and str.matchAll (always). Both return match objects that carry an index property telling you exactly where the match sits in the text. With that index you can inspect the surrounding characters yourself and decide whether to keep the match.

let str = "3 candles cost 40€";

for (let m of str.matchAll(/\d+/g)) {
  // m.index is where the digits start; check the char right after the match
  let after = str[m.index + m[0].length];
  if (after === "€") {
    alert(m[0]); // 40
  }
}

This works, but it’s more code and more room for off-by-one mistakes. For anything beyond the trivial, a lookaround expresses the intent in one place and lets the engine handle the bookkeeping.

Summary

Lookaround lets you match something based on the context around it — before, after, or both — without consuming that context.

  • Lookahead tests what comes after your target.
  • Lookbehind tests what comes before it.
  • Both are zero-width: the tested text is checked but not included in the match (unless you deliberately wrap part of it in a capturing group).
  • For simple contexts you can match everything and filter by index in a loop, but lookaround is usually cleaner.

The four forms:

Pattern type matches
X(?=Y) Positive lookahead pattern:X if followed by pattern:Y
X(?!Y) Negative lookahead pattern:X if not followed by pattern:Y
(?<=Y)X Positive lookbehind pattern:X if preceded by pattern:Y
(?<!Y)X Negative lookbehind pattern:X if not preceded by pattern:Y