Most regex patterns match and consume characters as they go — each part of the pattern claims the text it matches, and the next part continues from where the previous one left off. Lookaheads and lookbehinds break that rule: they check that something is present nearby without including it in the match. That distinction is small to state and easy to misuse, so it's worth working through carefully.
The four assertions
| Syntax | Name | Matches when... |
|---|---|---|
X(?=Y) | Positive lookahead | X is followed by Y |
X(?!Y) | Negative lookahead | X is NOT followed by Y |
(?<=Y)X | Positive lookbehind | X is preceded by Y |
(?<!Y)X | Negative lookbehind | X is NOT preceded by Y |
In every case, Y — the lookaround part — is not part of the returned match and doesn't consume characters. That's the entire point: it's a condition on the surrounding text, checked and then discarded.
Why "non-consuming" matters
Consider matching a price that's followed by "USD" but you only want the number, not the currency label: \d+(?=\s*USD). Given the text "42 USD", this matches just "42" — the engine checks that "USD" follows, confirms it, and then reports only the digits as the match. Without the lookahead, \d+\s*USD would match "42 USD" in full, forcing you to strip the currency label afterward with extra code.
This is the practical value of lookaround: it lets a condition live in the pattern itself instead of in post-processing logic after the match.
A realistic example: password validation
A common request is "require at least one digit, one lowercase letter, one uppercase letter, and 8+ characters, in any order." Without lookaheads, you'd need to write separate checks and combine them in code. With them, a single pattern like this works:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$ Each (?=.*X) is a positive lookahead scanning ahead from the same starting position for a lowercase letter, an uppercase letter, and a digit respectively — none of them consume any characters, so all three can be checked from position zero before .{8,} finally consumes and matches the actual 8+ characters. This is the single most common real-world lookahead pattern you'll encounter.
Lookbehind: the newer, less-supported sibling
Lookbehind ((?<=Y)X and (?<!Y)X) does the same thing in reverse — it checks what comes before the match without consuming it. A common use: extracting a value after a label, like pulling "1234" out of "Order #1234" with (?<=#)\d+, without the # itself showing up in the result.
The caveat: lookbehind support came to JavaScript later than lookahead (ES2018) and some older engines or environments still don't support it, so check your target runtime if you're writing a pattern meant to run somewhere other than a modern browser or Node.js.
Negative lookaround: asserting absence
Negative lookahead/lookbehind confirm something is not there. A classic case: match a word that isn't immediately followed by a specific suffix, like matching "Type" only when it isn't part of "TypeScript": Type(?!Script). This matches "Type" in "Type theory" but not in "TypeScript".
Common mistakes
- Forgetting lookaround doesn't consume characters — if you expect the "Y" part of a lookahead to appear in your match, capture group, or replacement, it won't. It's checked and discarded.
- Using lookaround where a simple character class would do. If you just need "not a digit,"
\Dis simpler and faster than(?!\d).. Reach for lookaround when the condition is about surrounding context, not about the character itself. - Variable-length lookbehind. Some regex engines only support fixed-length lookbehind patterns (no
*or+inside). JavaScript's engine is more permissive than some others, but it's worth testing rather than assuming.
Test it yourself
Lookaround patterns are easy to get subtly wrong, so test against real sample text before shipping. The Regex Tester highlights matches live as you type the pattern, which makes it obvious when a lookahead is or isn't behaving the way you expect.