Anchors, boundaries & lookaround
Learn JavaScript regex anchors, word boundaries, lookahead, lookbehind, and inline modifiers by matching positions instead of characters.
- 01Match positions deliberatelyTrace where
^,$,\b,\B, and proposed buffer anchors can stand. - 02Validate whole inputsChoose anchors for entire fields instead of accidentally finding a substring inside them.
- 03Read lookaround-heavy regexesExplain positive and negative lookahead, lookbehind, captures inside them, and inline modifiers.
Positions, not characters
Regex lessons often begin with characters: letters, digits, classes, and repeats. This lesson switches the question. Instead of asking “which character can I eat next?”, anchors, boundaries, and lookaround ask “is the engine standing in the right place?”
A zero-width assertion is a regex part that checks a position and consumes no characters. Anchors such as ^ and $, word boundaries such as \b, and lookarounds such as (?=...) all match places between characters.
Think of a zero-width assertion as a guard posted in a doorway. People move through the doorway, but the guard is not one of the people. A regex engine can stand at a boundary, check what is before or after it, and still consume nothing.
- In real life: The guard stands between rooms
- In JavaScript: A zero-width assertion stands between characters
- In real life: The guard checks a badge
- In JavaScript: Lookahead or lookbehind checks nearby text
- In real life: The guard does not become part of the group
- In JavaScript: The assertion is not included in the consumed match
Where the analogy stops: A guard can remember a person and make decisions. A regex assertion only checks the nearby source text and then the engine continues from the same position.
We will stay in this lane: anchors, boundaries, lookahead, lookbehind, and inline modifiers. For the surrounding regex tools, revisit patterns and flags, character classes, quantifiers, and groups and backreferences.
| Tool | Question it answers | Consumes text? |
|---|---|---|
| Anchors | Am I at the start or end of an input or line? | No |
| Word boundaries | Am I between a word and non-word character? | No |
| Lookaround | Does text before or after this point match another pattern? | No |
| Inline modifiers | Should flags apply only inside this group? | The group may contain consuming tokens, but the modifier itself consumes nothing |
^, $ and multiline mode
STEP THROUGH^ and $ are the first anchors most JavaScript developers meet. Without the m flag, ^ is only the start of the whole input and $ is only the real end of the whole input. With m, they can also stand at line boundaries inside the same string.
Step through the same source with and without the m flag. Watch how the anchors move from the whole buffer to each line.
script
const pattern = /^beta$/m;const match = pattern.exec(text); console.log(match ? match.index : null);console.log(match ? match[0] : "no match");| Anchor | Without m | With m |
|---|---|---|
^ | Matches only index 0 of the whole input. | Also matches just after a line terminator. |
$ | Matches only the real end of the whole input in JavaScript. | Also matches just before a line terminator. |
Final \n subtlety | /end$/.test("end\n") is false in JavaScript. | /end$/m.test("end\n") is true because $ can stand before the newline. |
| Validation | Use without m for a whole input. | Use with m when each line is its own record. |
The final-newline detail is worth testing because different regex traditions disagree. In JavaScript, $ without m does not also match just before a final \n. The pattern must include the newline, or you must use multiline mode when line endings are the thing you want.
$ and a final newlineconsole.log(/end$/.test("end\n"));console.log(/end\n$/.test("end\n"));console.log(/end$/m.test("end\n"));Use unflagged anchors when validating a whole field. Use multiline anchors when scanning a log, textarea, or source file line by line. A bare search finds inside a value; an anchored search validates the whole value.
const value = "say admin now";console.log(/admin/.test(value));console.log(/^admin$/.test(value));console.log(/^admin$/.test("admin"));Word boundaries and proposed buffer boundaries
INTERACTIVE\b is a boundary between a JavaScript regex “word character” and a non-word character. In practice, that word side is ASCII-style: letters A through Z, a through z, digits, and underscore. The opposite assertion, \B, matches positions that are not word boundaries.
const text = "hi\ncafé cat\nend\n";console.log([...text.matchAll(/^/gm)].map((m) => m.index));console.log([...text.matchAll(/$/gm)].map((m) => m.index));console.log([...text.matchAll(/\b/g)].map((m) => m.index));With m, the same anchors also appear around line breaks, so line-oriented scans can work.
matchAll positions. Boxes are positions between code units; labels above them are zero-width matches.That ASCII basis matters with real names and languages. The word café looks like one word to a person, but \b does not treat é as an ASCII word character, even with the u flag. For Unicode words, use lookaround with Unicode property escapes from the character classes lesson.
\b and a Unicode-friendly alternativeconsole.log(/\bcafé\b/u.test("café"));console.log(/(?<!\p{L})café(?!\p{L})/u.test("café"));console.log(/\Bend/.test("weekend"));As of September 26, 2026, TC39’s RegExp Buffer Boundaries proposal for \A, \z, and \Z is Stage 3. It is not supported in Node 22 or Chrome 154 today. \A would mean start of the whole buffer, \z absolute end, and \Z end or just before one final line terminator.
\A, \z, and \ZJavaScript// Proposal syntax: not supported in Node 22 or Chrome 154 today.const log = "status: ok\n";console.log(/\Astatus: ok\Z/u.test(log)); // would allow one final line terminatorconsole.log(/\Astatus: ok\z/u.test(log)); // would require the absolute endToday, use ^ and $ without m for most whole-input checks, or the absolute-position lookarounds below when you need an anchor that cannot be affected by flags around it.
const exact = "status: ok";const withFinalNewline = "status: ok\n"; console.log(/^status: ok$/.test(exact));console.log(/(?<![\s\S])status: ok(?![\s\S])/.test(exact));console.log(/(?<![\s\S])status: ok(?![\s\S])/.test(withFinalNewline));Lookahead: check what comes next
STEP THROUGHPositive lookahead (?=...) says “the next text must match this.” Negative lookahead (?!...) says “the next text must not match this.” Either way, the engine stays at the same position after the assertion succeeds.
const text = "ababa";const starts = [...text.matchAll(/(?=aba)/g)].map((match) => match.index);console.log(starts.join(","));console.log(text.replace(/(?=aba)/g, "|")); const captured = /(?=(aba))/.exec(text);console.log(captured[0].length);console.log(captured[1]);The first line finds two start positions for aba in ababa: 0 and 2. The replacement inserts markers at those positions without deleting any letters. The final two logs show a subtle but useful rule: captures inside a positive lookahead still capture. The whole assertion match is empty, but group 1 holds aba.
Lookahead lets several rules inspect the same input before the consuming part runs. Step through the three passwords.
script
console.log(strong.test("desk-2"));console.log(strong.test("Desk 2026"));console.log(strong.test("Desk-2026"));Lookahead is also useful for formatting. The thousands separator pattern places a comma at a non-boundary position only when complete groups of three digits are ahead and no extra digit follows that group.
\B and lookaheadconst value = "1234567890";console.log(value.replace(/\B(?=(\d{3})+(?!\d))/g, ","));Lookbehind: check what came before
PRACTICALPositive lookbehind (?<=...) says “the previous text must match this.” Negative lookbehind (?<!...) says “the previous text must not match this.” Current Chrome, Firefox, Edge, and Safari support lookbehind; Safari support begins at 16.4, so old Safari is the main browser caveat.
const receipt = "Tea $3.50, mug $12, tax 0.90";const prices = [...receipt.matchAll(/(?<=\$)\d+(?:\.\d{2})?/g)].map((match) => match[0]);console.log(prices.join(", "));The dollar sign is checked but not consumed. That makes lookbehind a clean fit for extraction: keep context out of the returned match without using a separate capturing group after the match.
const strong = /^(?=.{8,}$)(?=.*[A-Z])(?=.*\d)(?!.*\s).+$/;console.log(strong.test("desk-2"));console.log(strong.test("Desk 2026"));console.log(strong.test("Desk-2026"));passes
The anchors make this a whole-password rule. Lookaheads check length, uppercase, digit, and no spaces before .+ consumes.
Inline modifiers: flags for only one group
ES2025Inline modifiers use syntax such as (?i:...), (?-i:...), and (?m:...) to turn flags on or off for one group. They are ES2025 syntax. The current compatibility picture is uneven: current Chrome and Firefox support them, Safari does not yet according to MDN compatibility data, and Node 22 throws “Invalid group.”
// ES2025 inline modifiers. Chrome 154 and current Firefox parse this; Node 22 and Safari do not.const route = /^(?i:api)-(?-i:v1)$/i;console.log(route.test("API-v1"));console.log(route.test("API-V1"));checking supportchecking supportChecking this browser before building the regex keeps server rendering and hydration stable.
The fallback below shows why this feature is useful. A plain global i flag can make an entire pattern case-insensitive, but it cannot make only api loose while keeping v1 exact.
const route = /^api-v1$/i;console.log(route.test("API-v1"));console.log(route.test("API-V1")); // cannot keep only v1 case-sensitivePractical patterns you will actually write
SORT ITThese assertions are not trivia. They separate “find this somewhere” from “this whole value is valid,” keep prefixes out of extracted text, and make replacement patterns land at positions rather than over characters.
| Use case | Pattern shape | Why |
|---|---|---|
| Whole-field validation | /^admin$/ | Rejects say admin now; good for one field value. |
| Line scanning | /^ERROR:/gm | Finds records that begin a line in a log. |
| Password rules | ^(?=.{8,}$)(?=.*\d).+$ | Several assertions inspect the same input before consuming it. |
| Whole-word highlighting | \bcat\b or Unicode lookaround | Use \b for ASCII words; use \p{L} lookaround for real words. |
| Extract prices | (?<=\$)\d+(?:\.\d{2})? | Keeps the dollar sign out of the match. |
^\b\d+(?<=\$)\Aadmin
Sort each regex piece by what it does. Proposal-only items are not supported by today's engines in this lesson.
Common misconceptions
- “
$always matches before a final newline.” Not in JavaScript withoutm. Test it before copying advice from another regex flavor. - “
^...$is always validation.” It validates the whole input only whenmis off and your pattern handles every allowed character. - “
\bmeans human word.” It is based on ASCII-style word characters, so names likecaféneed a Unicode-aware alternative. - “Lookaround eats the text it checks.” It does not. Consuming tokens after the assertion decide what appears in the match.
- “Unsupported regex syntax is harmless if it is never reached.” Syntax in a regex literal is parsed before code runs. Store new syntax as a string and feature-detect when a runtime may not parse it.
| Syntax | Name | Condition | Common use |
|---|---|---|---|
(?=...) | Positive lookahead | The next characters must match, but they are not consumed. | Password rules, separators before a group |
(?!...) | Negative lookahead | The next characters must not match. | Reject whitespace or stop a digit run |
(?<=...) | Positive lookbehind | The previous characters must match, but stay outside the result. | Numbers after $ |
(?<!...) | Negative lookbehind | The previous characters must not match. | Unicode whole-word checks with \p{L} |
Practice exercises
5 EXERCISES$ with a final newlineRead the three tests and type the three booleans in order.
console.log(/end$/.test("end\n"));
console.log(/end\n$/.test("end\n"));
console.log(/end$/m.test("end\n"));The outputs are false, true, and true: JavaScript's unflagged $ does not match before the final newline, the second pattern includes it, and m allows $ before a line break.
Which of the two logged checks is the Unicode-friendly whole-word test?
console.log(/\bcafé\b/u.test("café"));
console.log(/(?<!\p{L})café(?!\p{L})/u.test("café"));The second check accepts café as a whole word. The first fails because é is not treated as an ASCII word character for \b.
Predict the formatted string.
const value = "1234567";
console.log(value.replace(/\B(?=(\d{3})+(?!\d))/g, ","));The replacement inserts commas at the two non-boundary positions before groups of three digits, so the output is 1,234,567.
Type the two extracted prices in order.
const receipt = "Tea $3.50, mug $12, tax 0.90";
const prices = [...receipt.matchAll(/(?<=\$)\d+(?:\.\d{2})?/g)].map((match) => match[0]);
console.log(prices.join(", "));Only the two numbers after dollar signs are returned: 3.50, 12. The tax value is ignored.
Which assertion technique checks several rules before the final consuming part of the regex?
const strong = /^(?=.{8,}$)(?=.*[A-Z])(?=.*\d)(?!.*\s).+$/;
console.log(strong.test("Desk-2026"));Use lookahead. Positive lookaheads can check length, uppercase, and digit rules before the consuming part runs; a negative lookahead rejects whitespace.
Check your understanding
7 QUESTIONSQuestion 1 of 7What makes anchors and lookaround different from a token like
\d?Choose an answer to see the explanation.
Question 2 of 7What does JavaScript print for the final-newline check?
Read the code, then predictconsole.log(/end$/.test("end\n"));Choose an answer to see the explanation.
Question 3 of 7What does multiline mode change here?
Read the code, then predictconsole.log(/^beta$/m.test("alpha\nbeta\ngamma"));Choose an answer to see the explanation.
Question 4 of 7Why does this word-boundary check fail?
Read the code, then predictconsole.log(/\bcafé\b/u.test("café"));Choose an answer to see the explanation.
Question 5 of 7What does the thousands-separator pattern print?
Read the code, then predictconsole.log("1234567".replace(/\B(?=(\d{3})+(?!\d))/g, ","));Choose an answer to see the explanation.
Question 6 of 7What does the price lookbehind extract?
Read the code, then predictconst text = "Tea $3.50, mug $12"; console.log([...text.matchAll(/(?<=\$)\d+(?:\.\d{2})?/g)].map((m) => m[0]).join(", "));Choose an answer to see the explanation.
Question 7 of 7Which statement is honest about inline modifiers today?
Choose an answer to see the explanation.
Key takeaways
- Anchors, boundaries, and lookaround are zero-width assertions: they check positions.
- JavaScript
$withoutmmatches the real end, not the spot before a final newline. \bis ASCII-oriented; use Unicode property lookaround for international words.- Lookahead and lookbehind do not consume, but captures inside successful positive lookaround can still capture.
- Inline modifiers are useful ES2025 syntax, but feature-detect because Node 22 and Safari do not parse them yet.
Remember the one-liner.
Assertions let a regex match the place where something is true, not only the characters that should be returned.
Up next: Regex performance & safety, where backtracking, ReDoS, and the sticky y flag explain why some patterns are safer than others.