(?<=...): positive lookbehind and where it runs
(?<=...) succeeds when the text immediately before the current position matches the pattern inside it, and consumes nothing. JavaScript allows the inner pattern to be any length.
At a glance
- Syntax
- (?<=...)
- Added in
- ES2018
- Length
- variable length is allowed, unlike Java
- Direction
- the inner pattern is matched from right to left
- Width
- zero, the match itself contains nothing from the lookbehind
- Not available in
- RE2 based engines, so Go's regexp package will not compile it
In detail
The everyday use is stripping a prefix from the match without stripping it from the string. /(?<=\$)\d+/ against "price: $30" returns just the digits, with the currency symbol confirmed but left out. Doing the same with a capture group works too, and the lookbehind version avoids the extra indirection of reading m[1] on every result.
JavaScript's implementation is unusually permissive about length. Java restricts lookbehind to a bounded pattern and several other engines refuse anything variable, but here the inner pattern can contain unbounded quantifiers. That makes some otherwise awkward patterns straightforward and means a JavaScript lookbehind will not necessarily port anywhere.
Inside the lookbehind, matching runs from right to left. That is invisible for a fixed string and very visible with a greedy quantifier and a capture: /(?<=(\d+)(\d+))$/ against "1053" leaves group one holding 1 and group two holding 053, which is the mirror image of what the same groups would capture reading forwards.
The support caveat matters when the pattern is shared. Engines built on RE2, which includes Go's standard regexp package, have no lookaround of any kind by design, because the guaranteed linear time matching depends on not having it. A pattern that has to run in both places cannot use lookbehind at all, and the workaround is usually a capture group plus a slice.
Examples
"price: $30".match(/(?<=\$)\d+/)[0]; // "30"
"img_001.png".replace(/(?<=img_)\d+/, "042"); // "img_042.png"Array.from(/(?<=(\d+)(\d+))$/.exec("1053"));
// ["", "1", "053"]Reading forwards, a greedy first group would have taken 105. Inside a lookbehind the second group matches first.
Things that catch people out
- Greedy quantifiers inside a lookbehind resolve from the right, so the captures come out in the opposite distribution to the one you would predict reading left to right.
- Lookbehind arrived in ES2018 and is absent from RE2 based engines, so a pattern using it will not compile in Go and cannot be shared with code that runs there.
- A lookbehind that could match an empty string always succeeds, which turns an intended prefix requirement into no requirement at all.
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 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.
(?=...): positive lookahead for zero width tests
A lookahead asserts what comes next without consuming it, which is how several independent rules can all be checked from the same position.
Anchoring to a line without turning on the m flag
How lookaround assertions give you per line anchors at one position in a pattern while ^ and $ keep their whole string meaning everywhere else.
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.
\t: matching tabs in TSV and indented text
\t matches the horizontal tab. Splitting a tab separated row on \s+ instead destroys every empty field, and JavaScript has no \h shorthand.
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