devkult_
tools29converters34
home/blog/regex-lookahead-lookbehind
regexjavascript

Regex Lookahead and Lookbehind, Explained with Examples

Most of a regular expression consumes text: \d eats a digit, [a-z]+ eats letters. Lookarounds are different. They check what's ahead of or behind the current position without consuming anything — so they let you say "match this, but only if…" without the condition becoming part of the match.

The four lookarounds

Syntax Name Succeeds when…
(?=…) positive lookahead what follows matches …
(?!…) negative lookahead what follows does not match …
(?<=…) positive lookbehind what precedes matches …
(?<!…) negative lookbehind what precedes does not match …

Because they're zero-width, a lookaround is like an if attached to a position.

Example 1: password rules with lookaheads

"At least 8 characters, with a digit and an uppercase letter" is hard to write as one sequence, because the requirements can be satisfied in any order. Lookaheads at the start of the string check each rule independently:

const strong = /^(?=.*\d)(?=.*[A-Z]).{8,}$/;

strong.test("hunter22");   // false — no uppercase letter
strong.test("Hunter22");   // true
strong.test("Hunter");     // false — no digit, too short

Each (?=.*X) peeks ahead from position 0, then the matcher returns to position 0 for the next check. Only .{8,} actually consumes anything. (Whether such rules are a good idea is another matter — see the password regex page.)

Example 2: "not followed by" with negative lookahead

Match .js files but not minified ones:

const source = /^(?!.*\.min\.js$).*\.js$/;

source.test("app.js");      // true
source.test("app.min.js");  // false

The negative lookahead at the start rejects the whole string if it ends in .min.js anywhere ahead.

Example 3: extract a value without its prefix

A positive lookbehind matches a price without including the dollar sign:

"Total: $42.50".match(/(?<=\$)\d+(?:\.\d{2})?/)[0];  // "42.50"

Without lookbehind you'd capture the number in a group and read match[1]. The lookbehind keeps the match itself clean, which is handy with replace() and matchAll().

Example 4: thousands separators

This classic uses both kinds of lookahead to find the positions where commas belong — it inserts text without matching any:

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

Read it as: at a position that isn't a word boundary (\B), where what follows is one or more groups of exactly three digits ((\d{3})+) and then no more digits ((?!\d)).

Example 5: label edges in a domain name

Domain labels can contain hyphens but can't start or end with one. The domain name regex enforces both edges with a lookahead and a lookbehind around the label:

(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.

(?!-) refuses a hyphen at the start; (?<!-) checks that the character before the dot isn't a hyphen.

Engine support

Engine Lookahead Lookbehind
JavaScript yes yes, since ES2018 — every current browser (Safari from 16.4) and Node.js
Python re yes fixed-width only; the third-party regex module allows variable width
Java yes bounded width (no * or + inside)
.NET yes yes, any width
PCRE / PHP yes fixed width per alternative in most versions
Go (RE2) no no — RE2 guarantees linear time by leaving out lookarounds and backreferences

If you target Go — or Rust's regex crate, which follows RE2 — do the extra check in code instead.

A performance note

(?=.*X) scans to the end of the input and backtracks, once per lookahead. On short strings like passwords that's irrelevant. On large inputs, or unanchored patterns run thousands of times, anchor the lookaheads with ^ so each runs once from the start, not at every position.

Try them

Paste any of these patterns into the regex tester to watch what matches as you type. For a broader introduction, see how to read any regex.

More from the blog