ZairussalamTools

The Developer's Guide to Regex Lookaheads and Lookbehinds

Positive and negative lookarounds explained with practical JavaScript examples — password validation, currency matching, URL slug extraction, and more.

·Ibrahimsyah Zairussalam·

Most regex you write is "match this pattern." Lookarounds are "match this pattern, but only if (or only if not) something else is nearby." They're the tool that takes you from "regex I could have written in 2010" to "regex that actually solves the problem without a second pass in code."

Four flavors exist, all supported in modern JavaScript:

  • (?=...) — positive lookahead: assert what follows
  • (?!...) — negative lookahead: assert what does not follow
  • (?<=...) — positive lookbehind: assert what precedes
  • (?<!...) — negative lookbehind: assert what does not precede

The critical thing: lookarounds don't consume characters. They're assertions. The regex engine peeks, confirms, and continues from where it was. That's what makes them powerful — and occasionally confusing.

Engine support note

Lookaheads have been in JavaScript regex forever. Lookbehinds (both flavors) were standardized in ES2018 and are supported everywhere modern — Node 10+, Chrome, Firefox, Safari 16.4+. If you're targeting truly ancient environments, test first. For everyone else, they're safe to use.

Positive Lookahead: "Only Match If Followed By..."

The classic use case is password validation. You want a string that contains at least one digit, at least one uppercase letter, at least one special character, and is at least 12 characters long. A single regex can check all of these by stacking positive lookaheads at the start.

Password validation with stacked lookaheads

Read the lookaheads from left to right:

  • (?=.*\d) — somewhere ahead there's a digit
  • (?=.*[A-Z]) — somewhere ahead there's an uppercase letter
  • (?=.*[!@#$%^&*]) — somewhere ahead there's a special character

All three assertions apply to position 0 (the ^ start anchor), so they all check the entire string from the beginning. None of them consume characters, so the [A-Za-z\d!@#$%^&*]{12,}$ part gets to match from position 0 as well.

This is the idiomatic way to do password validation in a single pass. The alternative is three separate .test() calls in JavaScript — more code, same result, slightly less clever.

Negative Lookahead: "Only Match If NOT Followed By..."

Find all prices not preceded by a currency symbol — say, to flag numbers in a document that look like money but lack proper formatting.

Actually, that's a lookbehind problem. Let's flip it: find all version numbers that are not followed by a hyphen suffix like -alpha or -beta — just the clean releases.

Matching clean version numbers

The (?!-) at the end says "this version number is only a match if the next character is not a hyphen." The lookahead doesn't consume the following character, so if the next thing is a space or punctuation, the match succeeds without capturing it.

Another classic: matching words that aren't part of a blocklist. You want to find every delete that isn't delete_from_archive:

Exclude a specific phrase

Positive Lookbehind: "Only Match If Preceded By..."

Now the prices-preceded-by-a-currency example, done right.

Match amounts preceded by a dollar sign

The (?<=\$) asserts "a $ is directly behind this position." Notice that $ isn't part of the match — the lookbehind doesn't consume it, so match() returns the number without the symbol. That's often exactly what you want when you're about to parseFloat() the result.

Compare that to capturing the $ and then stripping it in code. The lookbehind version is less code and harder to get wrong.

Negative Lookbehind: "Only Match If NOT Preceded By..."

Use case: extracting URL slugs from paths, but only for articles (not user profiles).

Extract article slugs only

The (?<!\/users) says "this slash is only the start of a match if /users is not directly behind it." Paths starting with /users/ are excluded; everything else passes.

This pattern shows up constantly in routing, logging filters, and content extraction. Doing it without lookbehind means matching everything and then filtering in code — more lines, same result, and easier to leak edge cases.

Combining Them

Lookaheads and lookbehinds compose cleanly. You can chain them at any position in a pattern. A contrived-but-useful example: extract every number that's between a $ and USD, without capturing either.

Number between two markers

The (?<=\$) and (?= USD) bookend the actual match. Only numbers between those two markers are captured, and neither marker is in the result.

Performance Reality Check

Lookarounds don't have a big performance cost in practice — the regex engine is usually smart about them — but a few things do matter:

  • Variable-length lookbehinds used to be unsupported in many engines. V8 supports them now. Older JS runtimes (IE, old Node) do not. If you're shipping to a known-modern target, fine.
  • Catastrophic backtracking can still happen if the inside of a lookaround is itself a nasty pattern. (?=(a+)+b) is a bad idea for the same reason (a+)+b is a bad idea outside one.
  • Unicode property escapes (\p{Letter}) inside lookarounds need the u flag. If you're matching international text, add /u.

When to Reach for Them

Lookarounds are worth it when the alternative is:

  • A two-step pipeline (regex then filter) that's actually one logical operation.
  • A capture group just to throw away the captured character.
  • Multiple .test() calls where a single assertion chain would work.

They're not worth it when the pattern becomes unreadable. A lookaround buried five levels deep in a 200-character regex is a maintenance hazard. If your teammates can't read it six weeks later, simpler code with a comment beats clever regex.

Test lookarounds with real input before you ship them. The assertion-based nature makes dry-running in your head unreliable — you think you know what matches, then a trailing newline or an unescaped dot proves otherwise.

The Bottom Line

Lookaheads let you peek at what follows without consuming it. Lookbehinds do the same for what precedes. Negative versions invert both. You need them when a match depends on context but the context shouldn't be part of the result.

Stack them for multi-rule validation. Bookend them for content extraction. Test them in a sandbox before you commit.