(?<!...): negative lookbehind to exclude a prefix
(?<!...) succeeds at any position where the text immediately before it does not match the inner pattern. It consumes nothing and it always succeeds at the start of the string.
At a glance
- Syntax
- (?<!...)
- Succeeds when
- the text immediately before does not match the inner pattern
- At index 0
- succeeds, because there is nothing behind to match
- Width
- zero
- Absolute start anchor
- (?<![\s\S]), unaffected by the m flag
- Not available in
- RE2 based engines such as Go's regexp package
In detail
The obvious use is excluding a prefix: a number that is not preceded by a currency symbol, a word that is not preceded by a negation, a path segment that is not preceded by a dot. Against "$30 and 40" the pattern /(?<!\$)\b\d+/g returns only the second number, because the first one has a dollar sign behind it and the word boundary stops the engine sidestepping onto the second digit.
That word boundary is doing real work. Without it the engine can simply start one character later, where the preceding character is a digit rather than a dollar sign, and the assertion passes. Any negative lookbehind placed in front of a repeatable token needs something to stop the match sliding forward, which is usually \b or an anchor.
At index zero the assertion succeeds by default, since there is no text behind the position to match. Turning that on its head gives the absolute start anchor: (?<![\s\S]) asserts there is no character at all behind the current position, which is true only at index zero and stays true regardless of the m flag.
Like its positive counterpart it is unavailable in RE2 based engines, and unlike its positive counterpart it rarely has a clean rewrite. Excluding a prefix without lookbehind usually means matching the prefix, capturing what follows, and filtering in code, which is more work but portable to every engine.
Examples
"$30 and 40".match(/(?<!\$)\b\d+/g); // ["40"]Without the \b the engine would match the 0 of 30, because the character behind that position is a digit rather than a dollar sign.
/(?<![\s\S])abc/m.test("x\nabc"); // false
/(?<![\s\S])abc/m.test("abc"); // true
/^abc/m.test("x\nabc"); // true, the line anchor still matchesThings that catch people out
- A negative lookbehind in front of a repeatable token lets the match start one character later, where the assertion happens to pass. Pair it with \b or an anchor.
- At index zero the assertion always succeeds, so a pattern meant to require a prefix accidentally accepts the very first position in the string.
- There is no lookbehind at all in RE2 based engines, and no drop in replacement, so a pattern shared with Go code has to be restructured rather than translated.
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 →(?<=...): 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.
(?!...): 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.
\A and \z in JavaScript: what to use instead
JavaScript has no \A, \z or \Z. Here is what those anchors silently turn into, and the absolute start and end assertions that survive the m flag.
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