# 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:

```js
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](/regex/password) page.)

## Example 2: "not followed by" with negative lookahead

Match `.js` files but not minified ones:

```js
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:

```js
"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:

```js
"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](/regex/domain-name) 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](/tools/regex/regex-tester) to watch what matches as you type. For a broader introduction, see [how to read any regex](/blog/how-to-read-any-regex).
