/^/

Lesson 12 of 28

Lookahead

Use positive lookahead (?=...) to check what comes next in a string without consuming it — the trick behind requiring several conditions at once, like password strength rules.

7 min read

Every quantifier and character class you’ve used so far consumes characters — once matched, they become part of the result. Lookahead is different: it checks whether a pattern would match at the current position, without actually consuming any characters. It’s a way to peek ahead and say “continue only if what follows looks like this.”

The syntax

Positive lookahead is written (?=...). It succeeds if the pattern inside matches starting at the current position, but the match itself doesn’t include those characters:

const text = "width: 100px; height: 50em; margin: 20px;";
const pixelValues = text.match(/\d+(?=px)/g);
console.log(pixelValues); // ["100", "20"]

Notice "50" is excluded — it’s followed by em, not px — and notice that the matched values are just the digits, "100" and "20", not "100px" and "20px". The lookahead confirmed px was there without swallowing it.

width: 100px; height: 50em; margin: 20px;
2 matches

Zero-width means it doesn’t move the cursor

Think of (?=...) as a question the regex engine asks itself at a given position: “if I looked ahead from here, would this match?” Whatever the answer, the engine’s position in the string doesn’t change — nothing inside the lookahead becomes part of the overall match, and nothing inside it is consumed, so the same characters remain available for the rest of the pattern to match against.

/\d+(?=px)/.exec("100px")[0]; // "100" — not "100px"

This zero-width property is exactly what makes lookahead useful for checking several independent conditions at the same position, which brings us to its most famous use case.

Password strength validation

Suppose a password must be at least 8 characters long, and contain at least one lowercase letter, one uppercase letter, and one digit — but those requirements can appear in any order, anywhere in the string. A single linear pattern can’t express “somewhere in here” for three unordered conditions — but three lookaheads, stacked at the very start, can:

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

strong.test("password123");  // false — no uppercase letter
strong.test("Password123");  // true
strong.test("weakpass");     // false — no uppercase, no digit
strong.test("AnotherGood1"); // true

Each (?=.*X) is checked from the same starting position (right after ^) and doesn’t consume anything, so all three can “look ahead” over the same characters independently. Only after all three succeed does .{8,} actually consume the string and confirm it’s long enough.

password123 Password123 weakpass AnotherGood1
2 matches
Piece What it checks Consumes characters?
(?=.*[a-z]) At least one lowercase letter appears somewhere ahead No
(?=.*[A-Z]) At least one uppercase letter appears somewhere ahead No
(?=.*\d) At least one digit appears somewhere ahead No
.{8,} At least 8 characters total Yes

Tip

You can chain as many lookaheads as you need at the same position — each one is an independent, non-consuming check. This is the standard pattern for “must satisfy all of these conditions, in any order.”

Extract percentages

Write a pattern that matches a number only when it’s immediately followed by a % sign, without including the % in the match. Test it against "50% off, 12 items, 99% pure".

Show solution
const text = "50% off, 12 items, 99% pure";
const percentages = text.match(/\d+(?=%)/g);
console.log(percentages); // ["50", "99"]

\d+(?=%) matches one or more digits, but only where a % follows immediately — "12" is skipped because it’s followed by a space, not a percent sign. The lookahead confirms the % is there without consuming it, so it never shows up in the result.

What’s next

Positive lookahead checks that something does follow. Next up: negative lookahead, (?!...), for checking that something does not follow — perfect for filtering out unwanted matches.

Quick check

What makes (?=...) a "zero-width" assertion?

Why does password validation commonly chain multiple lookaheads, like ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$?