(?=...): positive lookahead for zero width tests
(?=...) succeeds when the pattern inside it matches at the current position, and then leaves the position exactly where it was. Nothing inside a lookahead becomes part of the match.
At a glance
- Syntax
- (?=...)
- Width
- zero, the position does not move
- Captures inside
- still numbered and still recorded in the result
- Atomic
- a lookahead that has succeeded is never retried with a different match
- Typical use
- stacked must contain assertions, and insertion points
- Not for
- capturing the text ahead, since it never joins the match
In detail
Because a lookahead consumes nothing, several of them in a row all run at the same position. That is what makes the password rule idiom work: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/ checks three independent conditions from index zero and then measures the length, and each assertion scans the whole string on its own without disturbing the others.
The same property makes it the right tool for finding an insertion point. A thousands separator is added with /\B(?=(\d{3})+(?!\d))/g: the pattern matches nothing at all, and a global replace inserts a comma at every position where the assertion holds. If it consumed the digits it would delete them.
Captures inside a lookahead behave normally. The group is numbered, it appears in the result and it can be referenced later, even though the characters it matched are not part of the overall match. That is the whole basis of the atomic group emulation, which relies on a lookahead capturing text and then re-consuming it with a backreference.
Being atomic is the property people rarely notice until they need it. Once a lookahead has succeeded the engine does not go back and try a shorter match inside it, so a greedy quantifier in there is effectively pinned at its longest successful length. That is useful deliberately and confusing accidentally.
Examples
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
strong.test("Passw0rdd"); // true
strong.test("password1"); // false, no uppercase letter"1234567".replace(/\B(?=(\d{3})+(?!\d))/g, ","); // "1,234,567"The pattern matches an empty string at each grouping point. The \B stops a comma appearing at the very start.
Things that catch people out
- A lookahead never contributes to the match, so building a pattern out of assertions alone gives you a list of empty matches rather than the text you were looking for.
- Quantifying a lookahead is legal and pointless. (?=x)* matches an empty string any number of times, which is once, and it slows the pattern down for nothing.
- Each stacked assertion scans the input independently, so five must contain rules on a long string do five passes. That is fine for a password field and worth measuring on a large document.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →(?!...): negative lookahead for exclusion rules
A negative lookahead succeeds when the pattern inside it fails. The classic bug is a greedy quantifier that backtracks until the assertion passes.
(?<=...): positive lookbehind and where it runs
JavaScript allows variable length lookbehind, which Java does not, and matches its contents from right to left. RE2 based engines have none at all.
Atomic groups in JavaScript: emulating them with a lookahead
JavaScript has no (?>...) and no possessive quantifiers. The lookahead and backreference pair gives you the same no backtracking behaviour.
(?<!...): negative lookbehind to exclude a prefix
A negative lookbehind succeeds when the text before the position does not match. At index zero it always succeeds, which is usually but not always right.
Alternation: precedence and ordered choice
The pipe has the lowest precedence of any regex operator and the branches are tried in order, so ^cat|dog$ and /a|ab/ both surprise people.
\n: matching a newline, and the double escape trap
\n matches the line feed character. The trap is building a pattern from a string, where the number of backslashes decides what the engine actually sees.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links