Combining lookarounds
Stack multiple lookaheads and lookbehinds at the same position to express precise, multi-condition matching — including the classic password validation pattern.
- Combine multiple lookaheads at the same position
- Build a password validation pattern using stacked lookaheads
- Judge when lookarounds simplify a pattern versus when they add unnecessary complexity
Each lookaround is a zero-width assertion that tests an independent condition without consuming characters. Because they are zero-width, you can place several at the same position — all must pass for the overall match to succeed. This lets you express patterns like "must contain at least one digit AND at least one uppercase letter AND be at least eight characters long."
Stacking lookaheads
Multiple (?=…) assertions before the main pattern act like AND conditions:
// Match a string that contains both "foo" and "bar" anywhere
/(?=.*foo)(?=.*bar)/.test("this has foo and bar"); // true
/(?=.*foo)(?=.*bar)/.test("this has foo only"); // false
/(?=.*foo)(?=.*bar)/.test("bar first, then foo"); // true — order doesn't matterEach lookahead is checked from the same starting position. The .* allows the
engine to scan ahead past any characters to find the required content.
The password validation pattern
A common real-world use case: validate that a string meets all of several independent character-type requirements.
const strongPassword = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
strongPassword.test("Passw0rd!"); // true — all criteria met
strongPassword.test("password1!"); // false — no uppercase
strongPassword.test("PASSWORD1!"); // false — no lowercase
strongPassword.test("Passw0rd"); // false — no special char
strongPassword.test("Pa1!"); // false — too shortBreaking down each lookahead:
| Assertion | Requirement |
|---|---|
(?=.*[A-Z]) | At least one uppercase letter somewhere |
(?=.*[a-z]) | At least one lowercase letter somewhere |
(?=.*\d) | At least one digit somewhere |
(?=.*[!@#$%^&*]) | At least one special character somewhere |
.{8,} | At least 8 characters total (the actual match) |
Each lookahead is anchored at the start (^) and scans forward with .*. They
are independent checks — order does not matter, and each one looks through the
entire string.
The password pattern is a canonical example because it is genuinely difficult to
express without lookaheads. An alternative approach — check each condition with a
separate test — is often clearer for developers reading the code. Use stacked
lookaheads when a single-pattern solution is required (for example, an <input pattern="…"> attribute), and prefer separate checks in application logic.
Combining positive and negative lookarounds
You can mix (?=…) (must be followed by) and (?!…) (must not be followed by)
at the same position:
// Match a word only if it is followed by "(" but not followed by "()"
/\w+(?=\()(?!\(\))/.test("func()"); // false — followed by ()
/\w+(?=\()(?!\(\))/.test("func(x)"); // true — followed by ( but not ()Similarly, lookbehind can be combined with lookahead:
// Extract numbers between $ and USD
"price: $42.50 USD each".match(/(?<=\$)[\d.]+(?=\s*USD)/);
// ["42.50"] — preceded by $ and followed by USDWhen lookarounds make patterns worse
Lookarounds are powerful but can also make patterns harder to read and debug. Consider the difference:
// With lookarounds: extracts just the number
"version: 2.14".match(/(?<=version:\s)[\d.]+/)[0]; // "2.14"
// With a capture group: arguably more obvious
"version: 2.14".match(/version:\s([\d.]+)/)[1]; // "2.14"Both work. The capture-group version makes the full context visible in the
pattern, while the lookbehind version produces a cleaner match. For a single
extraction, pick whichever reads more clearly in context. For complex multi-
condition gates where you cannot add capture groups (like a browser pattern
attribute), lookarounds are the right choice.
A practical multi-lookaround example
Extract only the values from a config string where the key is one of host,
port, or db:
const config = "host=localhost port=5432 db=myapp user=admin timeout=30";
// Match a word that is preceded by one of the accepted keys and "="
const values = config.match(/(?<=(?:host|port|db)=)\w+/g);
// ["localhost", "5432", "myapp"]The lookbehind contains a non-capturing group (?:host|port|db) with alternation
— the full lookbehind is (?<=(?:host|port|db)=).
Where to go next
The Lookarounds lab has hands-on exercises applying all four lookaround types to realistic strings. After that, you will have covered the full regex toolkit — from literals through lookarounds — and can tackle virtually any text-matching problem.