completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Anchors, boundaries & lookaround

Learn JavaScript regex anchors, word boundaries, lookahead, lookbehind, and inline modifiers by matching positions instead of characters.

By the end, you can
  • 01
    Find the gaps a regex checksPoint to the gaps where ^, $, \b, and \B can match, and know what the planned \A, \z, and \Z would do.
  • 02
    Check a whole valueUse anchors so a regex checks the whole form field, not just a piece of text inside it.
  • 03
    Read lookaroundsExplain lookahead and lookbehind, both positive and negative, what round brackets inside them save, and inline modifiers.

Positions, not characters

A regular expression, or regex, is a pattern for finding text. Most regex parts match characters. For example, \d matches one digit, and cat matches the three letters c, a, t.

This lesson is about a different kind of part. These parts don't match a character. They check a position: a gap between two characters, or the gap at the start or end of the text.

While a regex runs, JavaScript moves through the text from left to right. The program that does this is called the regex engine. A position check asks: "Is the engine standing in the right gap?"

Plain definition

A zero-width assertion is a regex part that checks a position. It uses up no characters, so it adds nothing to the match. That is why it is called "zero-width".

This lesson covers three kinds:

  • Anchors, like ^ and $: the start or end of the text.
  • Word boundaries, like \b: the edge of a word.
  • Lookarounds, like (?=...): what comes just before or just after the gap.
The pattern /^admin$/ tested on "admin". The five letters a, d, m, i, n are five cards. ^ sits in the gap before the first letter, at position 0. $ sits in the gap after the last letter, at position 5. The match is the 5 letters; ^ and $ add nothing./^admin$/.test("admin")^admin$position 05 letters matchposition 5
^ checks the gap before the first letter. $ checks the gap after the last letter. Neither one uses up a letter.
Real-life analogyA guard at a doorway

Picture a guard standing in a doorway. People walk through the doorway. The guard checks each badge, but the guard does not walk with them. The guard is never one of the people who went through.

In real life: The guard stands in the doorway between two rooms
In JavaScript: A zero-width assertion stands in the gap between two characters
In real life: The guard checks each person's badge
In JavaScript: A lookahead or lookbehind checks the text next to the gap
In real life: The guard does not join the group that walks through
In JavaScript: The assertion adds no characters to the match

This lesson is only about position checks. Other lessons cover the rest of regex:

This table lists the tools in this lesson. Read each row from left to right: the tool, the question it asks, and whether it uses up text.

The position-checking tools in this lesson
ToolWhat question does it ask?Does it use up text?
AnchorsAm I at the start or end of the text, or of a line?No.
Word boundariesAm I at the edge of a word?No.
LookaroundDoes the text just before or just after this gap match another pattern?No.
Inline modifiersShould a flag apply only inside this group?The modifier itself uses up nothing. The parts inside its group can still use up text.

^, $ and multiline mode

STEP THROUGH

^ and $ are the anchors you will meet most often. A flag is a letter after the closing slash that changes how the regex works. The m flag turns on multiline mode.

  • Without m: ^ means only the start of the whole text, and $ means only the end of the whole text.
  • With m: ^ also means the start of each line, and $ also means the end of each line.

In the code below, \n is a line break: one character that starts a new line. Step through it with and without m.

Step where ^ and $ can stand
Step 0 of 5Ready
Your turn: follow the blue line

Step through the same code with and without the m flag. Watch where ^ and $ are allowed to match.

Running in
  1. script
Next: line 1
const pattern = /^beta$/m;const match = pattern.exec(text); console.log(match ? match.index : null);console.log(match ? match[0] : "no match");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Turn the `m` flag on or off

Changing the flag starts the replay again. Guess first: does line 5 print an index or `null`?

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
The text "alpha\nbeta\n" drawn as two lines. Left, /^beta$/ without m: ^ can only stand before alpha, at the start of the whole text, and $ only after the last line break, so there is no match and exec gives null. Right, /^beta$/m: ^ can stand before beta and $ right after beta, so beta matches at index 6./^beta$/^alpha\nbeta\n$only at the two endsnull/^beta$/malpha\n^beta$\nalso at each linebeta at index 6
Without m, ^ and $ only fit at the two ends of the whole text, so beta has no match. With m, they also fit at the start and end of each line.

This table compares the two modes. Read each row from left to right: what you compare, what happens without m, and what happens with m.

How ^ and $ change with the m flag
What to compareWithout mWith m
^Matches only at index 0, the start of the whole text.Also matches just after each line break.
$Matches only at the very end of the whole text.Also matches just before each line break.
A last \n/end$/.test("end\n") is false, because $ is not at the very end./end$/m.test("end\n") is true, because $ can stand just before the line break.
When to use itTo check a whole value, like a form field.To check each line on its own, like the lines of a log file.

Some text ends with one last line break. Other regex languages, like Python and Perl, let $ match just before that last line break. JavaScript does not.

In JavaScript, $ without m matches only the very end. So if the text ends with \n, you have two choices:

  • Put the \n in your pattern.
  • Turn on m, if line ends are what you want.
JavaScript $ and a last line breakPop out in the code editor (opens in a new tab)JavaScript
console.log(/end$/.test("end\n"));console.log(/end\n$/.test("end\n"));console.log(/end$/m.test("end\n"));

Anchors also decide whether a regex finds text or checks the whole text:

  • /admin/ finds admin anywhere inside the text.
  • /^admin$/ passes only if the whole text is exactly admin.

Checking that the whole text follows a rule is called validation. For example, you validate a form field before you save it.

Finding inside the text vs checking the whole textPop out in the code editor (opens in a new tab)JavaScript
const value = "say admin now";console.log(/admin/.test(value));console.log(/^admin$/.test(value));console.log(/^admin$/.test("admin")); const lines = "admin\nsay admin now";console.log(/^admin$/.test(lines));console.log(/^admin$/m.test(lines));

Look at the last two logs. The text lines has two lines, and the first line is admin.

  • Line 7 has no m, so the whole text must be admin. It is not, so the result is false.
  • Line 8 has m, so one matching line is enough. The result is true.

So leave m off when you validate a form field. A user could type admin on one line and anything else on the next line, and it would pass. Turn m on only when each line is its own record, like the lines of a log file.

Word boundaries and planned buffer boundaries

INTERACTIVE

\b is a word boundary. It matches a gap where a word character is on one side and a non-word character (or the start or end of the text) is on the other side.

For \b, a word character is only one of these:

  • a letter from A to Z or a to z
  • a digit from 0 to 9
  • the underscore, _

The opposite is \B. It matches every gap that is not a word boundary. Try both in the playground below.

See where each check can match
Find the gaps with matchAllPop out in the code editor (opens in a new tab)JavaScript
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));
Gaps7 matches
0^mh
1·i
2$mLF
3^mc
4·a
5·f
6·é
7·space
8·c
9·a
10·t
11$mLF
12^me
13·n
14·d
15$mLF
16^m $mend
Try it yourself
Up to 80 characters. The lesson picks the regexes; you only change the text.

With m, ^ and $ also show up at each line break, so you can check each line on its own.

Each column is one gap in the text, with its index on top. A mark in the middle means the check matched in that gap. The character below is the one just after the gap.

The short list of word characters causes trouble with real names. A person reads café as one word. But é is not on the list, so \b sees a word edge between f and é. The u flag does not change this.

For words in any language, use lookarounds with \p{L}. With the u flag, \p{L} means "any letter in any language". You met it in the Character classes lesson. Lookarounds are explained later in this lesson.

\b only knows A to Z; \p{L} knows every letterPop out in the code editor (opens in a new tab)JavaScript
console.log(/\bcat\b/.test("concatenate"));console.log(/\bcat\b/.test("a cat"));console.log(/\bcafé\b/u.test("café"));console.log(/(?<!\p{L})café(?!\p{L})/u.test("café"));console.log(/\Bend/.test("weekend"));

Here is what each log shows:

  • Line 1 gives false. Inside concatenate, the letters cat have letters on both sides, so there is no word edge.
  • Line 2 gives true. In a cat, the word cat stands on its own.
  • Line 3 gives false. \b does not count é as a word character.
  • Line 4 gives true. The lookarounds check that no letter of any language comes before or after café.
  • Line 5 gives true. In weekend, the gap before end has letters on both sides, so it is not a word edge, and \B matches it.
Buffer boundaries are only a proposal

TC39 is the group that adds new features to JavaScript. A proposal is a planned feature that is not finished yet. The RegExp Buffer Boundaries proposal is at Stage 3, close to finished. Node 22 and Chrome 154 do not support it yet.

Here, the "buffer" means the whole text. The proposal adds three new anchors:

  • \A: the start of the whole text.
  • \z: the very end of the whole text.
  • \Z: the end, or just before one last line break.
Planned syntax: \A, \z, and \Z (does not run yet)JavaScript
// Planned syntax: Node 22 and Chrome 154 can't run this yet.const log = "status: ok\n";console.log(/\Astatus: ok\Z/u.test(log)); // would allow one last line breakconsole.log(/\Astatus: ok\z/u.test(log)); // would need the very end

Here is what works today:

  • For most whole-text checks, use ^ and $ without m.
  • If you need an anchor that no flag can change, use the lookarounds on lines 5 and 6 below. [\s\S] means "any character". So "no character before" means the start, and "no character after" means the very end.
Whole-text checks that work todayPop out in the code editor (opens in a new tab)JavaScript
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 THROUGH

A lookahead checks the text just after the current gap. It comes in two kinds:

  • Positive lookahead, (?=...): the next text must match the pattern inside.
  • Negative lookahead, (?!...): the next text must not match the pattern inside.

After the check passes, the engine stays in the same gap. It has not moved past any characters.

A lookahead checks but does not use up textPop out in the code editor (opens in a new tab)JavaScript
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]); const noAdmin = /^(?!.*\badmin\b).+$/;console.log(noAdmin.test("admin user"));console.log(noAdmin.test("regular user"));

Here is what each part shows:

  • Line 3 prints 0,2. The lookahead finds two gaps in ababa where aba comes next: index 0 and index 2.
  • Line 4 prints |ab|aba. replace puts a | in each of those gaps. No letters are removed, because the lookahead matched no letters.
  • Lines 6 to 8 show that round brackets inside a positive lookahead still save text. The whole match is empty, so its length is 0. But group 1 saved aba.
  • Lines 10 to 12 use a negative lookahead as a "must not contain" rule. (?!.*\badmin\b) fails if the word admin appears anywhere. So admin user gives false, and regular user gives true.

In the last example, the lookahead checks from the start first. Only after the check passes does .+ use up the text.

Lookaheads are great for password rules. Several lookaheads can all check the same password from the start, one rule each. Step through it below.

Step through the password rules
Step 0 of 4Ready
Your turn: follow the blue line

Several lookaheads check the same password before .+ uses it up. Step through the three passwords.

Running in
  1. script
Next: line 1
console.log(strong.test("desk-2"));console.log(strong.test("Desk 2026"));console.log(strong.test("Desk-2026"));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Lookaheads can also add commas to a long number, like 1,234,567,890. The pattern finds each gap where the digits to the right come in full groups of three:

  • \B means "not a word edge". It stops a comma at the very start.
  • (?=(\d{3})+ checks that one or more groups of three digits come next.
  • (?!\d)) checks that no extra digit comes after those groups, so the groups end exactly at the last digit.
Add commas with \B and a lookaheadPop out in the code editor (opens in a new tab)JavaScript
const value = "1234567890";console.log(value.replace(/\B(?=(\d{3})+(?!\d))/g, ","));
The pattern /\B(?=(\d{3})+(?!\d))/g on "1234567890". The digits split into 1, 234, 567, and 890. A comma goes into each gap where the digits to the right come in full groups of three. The result is 1,234,567,890./\B(?=(\d{3})+(?!\d))/g3 digits3 digits3 digits1,234,567,8901,234,567,890
At each gap, the lookahead looks at the digits to the right. A comma goes in only where they come in full groups of three.

Lookbehind: check what came before

PRACTICAL

A lookbehind checks the text just before the current gap. It also comes in two kinds:

  • Positive lookbehind, (?<=...): the text before must match the pattern inside.
  • Negative lookbehind, (?<!...): the text before must not match the pattern inside.

All current browsers support lookbehind: Chrome, Firefox, Edge, and Safari. Safari added it in version 16.4, so only older Safari versions lack it.

Get the numbers after a dollar signPop out in the code editor (opens in a new tab)JavaScript
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 pattern /(?<=\$)\d+(?:\.\d{2})?/g on "$3.50". The lookbehind checks that a dollar sign comes just before the number, but does not keep it. The digits 3.50 are kept, so match[0] is 3.50./(?<=\$)\d+(?:\.\d{2})?/gchecked$kept3.503.50match[0]
The lookbehind checks that a $ comes just before the number. The $ is not part of the match, so you get 3.50.

The lookbehind checks the dollar sign but does not use it up. So the match holds only the number. You don't need an extra group to cut the $ off. The number 0.90 has no $ before it, so it is skipped.

Try lookarounds on real jobs
The pattern you pickedPop out in the code editor (opens in a new tab)JavaScript
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"));
Outputpassword

passes

Try it yourself

^ and $ make the rules apply to the whole password. The lookaheads check the length, a capital letter, a digit, and no spaces. Only then does .+ use up the text.

The lesson picks the regexes and limits how long your text can be. You change the text and watch the result.

Inline modifiers: flags for only one group

ES2025

A flag like i (ignore upper and lower case) usually applies to the whole regex. An inline modifier turns a flag on or off for just one group:

  • (?i:...) turns i on inside the group.
  • (?-i:...) turns i off inside the group.
  • (?m:...) turns m on inside the group.

Inline modifiers were added in ES2025, the 2025 edition of JavaScript. Not every place supports them yet:

  • Current Chrome and Firefox support them.
  • Safari does not yet, says MDN's browser support table.
  • Node 22 stops with the error "Invalid group".
Does this browser support inline modifiers?
Inline modifiers: one flag per groupPop out in the code editor (opens in a new tab)JavaScript
// ES2025 inline modifiers. Chrome 154 and Firefox can read this; Node 22 and Safari can't.const route = /^(?i:api)-(?-i:v1)$/i;console.log(route.test("API-v1"));console.log(route.test("API-V1"));
Resultchecking
API-v1checking this browser
API-V1checking this browser
Try it yourself
Tested with `new RegExp` inside `try`

The page waits until it is in your browser before it tests anything.

The code shows the new syntax. The panel first tests whether your browser can read it, so the lesson still works in browsers that can't.

The older way below shows why this feature helps. A normal i flag makes the whole pattern ignore case. It can't ignore case for api while keeping v1 exact.

The older way: one flag for the whole patternPop out in the code editor (opens in a new tab)JavaScript
const route = /^api-v1$/i;console.log(route.test("API-v1"));console.log(route.test("API-V1")); // can't keep only v1 exact

Patterns you will really write

SORT IT

You will use these position checks in everyday code:

  • to check that a whole value is valid, not just that it contains something
  • to leave a prefix like $ out of the text you pull out
  • to insert text into gaps, like commas, without removing any characters

This table shows common jobs. Read each row from left to right: the job, the pattern, and why it works.

Which position check to use for each job
What job?What pattern?Why does it work?
Check a whole form field/^admin$/It rejects say admin now. The whole value must be exactly admin.
Find lines in a log/^ERROR:/gmWith m, ^ matches at the start of each line, so it finds every line that starts with ERROR:.
Password rules^(?=.{8,}$)(?=.*\d).+$Each lookahead checks one rule on the same password before .+ uses it up.
Highlight whole words\bcat\b, or lookarounds with \p{L}\b works for words made of A to Z. Use \p{L} lookarounds for words in any language.
Get prices(?<=\$)\d+(?:\.\d{2})?The lookbehind checks for the $ but keeps it out of the match.
Uses up characters or checks a position?
  • ^
  • \b
  • \d+
  • (?<=\$)
  • \A
  • admin
Try it yourself
0 of 6 correct

Sort each regex part by what it does. A part that only exists in a proposal does not run in today's engines.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • "$ always matches before a last line break." Not in JavaScript without m. Other regex languages do this, so test advice before you copy it.
  • "^...$ always checks the whole value." Only when m is off, and only if your pattern covers every allowed character.
  • "\b finds the edge of any word." It only knows the letters A to Z, digits, and _. Words like café need lookarounds with \p{L}.
  • "A lookaround uses up the text it checks." It does not. Only the parts after it decide what ends up in the match.
  • "New regex syntax is safe if that code never runs." JavaScript reads a regex literal before any code runs, so new syntax can crash the whole file. Keep new syntax in a string and test for support first.

This table lists all four lookarounds. Read each row from left to right: how you write it, its name, what it checks, and a common use.

The four lookarounds
How you write itNameWhat does it check?Common use
(?=...)Positive lookaheadThe text just after the gap must match. It is checked, not used up.Password rules, and commas before groups of three digits.
(?!...)Negative lookaheadThe text just after the gap must not match.Reject spaces, or stop a run of digits.
(?<=...)Positive lookbehindThe text just before the gap must match. It stays out of the result.Get numbers that come after a $.
(?<!...)Negative lookbehindThe text just before the gap must not match.Whole-word checks for any language, with \p{L}.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict $ with a last line break

Read the three tests. Type the three logged values in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log(/end$/.test("end\n"));
console.log(/end\n$/.test("end\n"));
console.log(/end$/m.test("end\n"));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePick the word check that works for all languages

    Which of the two checks treats café as one whole word?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    console.log(/\bcafé\b/u.test("café"));
    console.log(/(?<!\p{L})café(?!\p{L})/u.test("café"));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd commas to a number

      Predict the string that prints.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const value = "1234567";
      console.log(value.replace(/\B(?=(\d{3})+(?!\d))/g, ","));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeGet the prices without the dollar sign

        Type the two prices that come back, in order.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        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(", "));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeName the tool behind the password rules

          Which kind of check tests several rules before the last part of the regex uses up the text?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const strong = /^(?=.{8,}$)(?=.*[A-Z])(?=.*\d)(?!.*\s).+$/;
          console.log(strong.test("Desk-2026"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7How are anchors and lookarounds different from a regex part like \d?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does JavaScript print when the text ends with a line break?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(/end$/.test("end\n"));

              Choose an answer to see the explanation.

            3. Question 3 of 7What does this print with the m flag?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(/^beta$/m.test("alpha\nbeta\ngamma"));

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this word-edge check print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(/\bcafé\b/u.test("café"));

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the comma pattern print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log("1234567".replace(/\B(?=(\d{3})+(?!\d))/g, ","));

              Choose an answer to see the explanation.

            6. Question 6 of 7What does the price lookbehind give back?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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.

            7. Question 7 of 7Which sentence about inline modifiers is true today?

              Choose an answer to see the explanation.

            Key takeaways

            • Anchors, word boundaries, and lookarounds are zero-width assertions. They check a gap and use up no text.
            • In JavaScript, $ without m matches only the very end, not the gap before a last line break.
            • \b only knows A to Z, digits, and _. For words in any language, use lookarounds with \p{L}.
            • Lookaheads and lookbehinds use up no text. Round brackets inside a positive lookaround can still save text.
            • Inline modifiers are new in ES2025. Test for support first, because Node 22 and Safari can't read them yet.

            Remember the one-liner.
            A position check lets a regex match the gap where something is true, not just the characters you get back.

            Up next: Regex performance & safety. It explains backtracking, ReDoS attacks, and the sticky y flag, and why some patterns are safer than others.

            CompleteFrontend Clear concepts. Working examples.