SKU // 89

JavaScript RegExp Assertions

Most regex pieces match characters. Assertions match a position instead: the start of the text, the edge of a word, "right before a digit." They check a condition without consuming any characters, which lets you say where a match is allowed to happen.

// ^ and $: Start and End

/^Hello/.test("Hello there")   // true   starts with Hello
/^Hello/.test("Oh, Hello")     // false
/\.pdf$/.test("notes.pdf")     // true   ends with .pdf

Using both anchors means "the whole text must match," which is what you want for validation:

/\d{3}/.test("abc12345")      // true   (3 digits appear somewhere)
/^\d{3}$/.test("abc12345")    // false  (the whole thing is not 3 digits)
/^\d{3}$/.test("123")         // true
true false true
Validation Rule
Always anchor validation patterns with ^ and $. Without them, /\d{5}/ happily accepts "my zip is 212019999 thanks" because five digits appear somewhere inside.

// \b: Word Boundary

\b matches the edge between a word character and a non-word character (or the start/end of the text). It is how you match whole words only:

const text = "cat concatenate bobcat cat.";

text.match(/cat/g).length        // 4  (inside other words too)
text.match(/\bcat\b/g).length    // 2  (only the standalone word)
4 2
// Replace the word "is", but not the "is" inside "This" or "island"
"This is an island".replace(/\bis\b/g, "IS");   // "This IS an island"

\B is the opposite: a position that is not a word edge.

"cat concatenate".match(/\Bcat\B/g)   // ["cat"]  only the one buried inside a word

\b works well with search() to find where a word starts or ends:

const text = "HELLO, LOOK AT YOU!";
console.log(text.search(/\bLO/));   // LO at the START of a word: LOOK
console.log(text.search(/LO\b/));   // LO at the END of a word: HELLO
7 3
Trap: \b only knows English letters
\b is defined by \w, so accented letters count as non-word characters. /\bcafé\b/.test("café") is false, even with the u flag, because there is no "edge" between é and the end of the text. For text in other languages, use lookarounds with \p{L} instead: /(?<!\p{L})café(?!\p{L})/u.

^ and $ with the m flag

With m, the anchors work on every line (see RegExp Flags):

const poem = "Is this\nall there\nis";
console.log(poem.match(/^is/gim));
["Is", "is"]

// Lookahead: (?=...) and (?!...)

A lookahead checks what comes next without including it in the match:

// (?=...)  positive: must be followed by
"100px 20em 5px".match(/\d+(?=px)/g)   // ["100", "5"]   numbers followed by px

// (?!...)  negative: must NOT be followed by
"100px 20em 5px".match(/\b\d+(?!\d|px)/g)   // ["20"]
["100", "5"] ["20"]

Notice the matches do not include "px". The lookahead only peeked.

// Lookbehind: (?<=...) and (?<!...)

The same idea, checking what came before:

// (?<=...)  must be preceded by
"Cost: $45, tax $3, qty 7".match(/(?<=\$)\d+/g)    // ["45", "3"]   amounts after $

// (?<!...)  must NOT be preceded by
"Cost: $45, tax $3, qty 7".match(/(?<![\$\d])\d+/g)   // ["7"]
["45", "3"] ["7"]

// Summary

^ $Start / end of text (of each line with the m flag)
\b \BWord edge / not a word edge
X(?=Y)X, only if followed by Y
X(?!Y)X, only if NOT followed by Y
(?<=Y)XX, only if preceded by Y
(?<!Y)XX, only if NOT preceded by Y

// Real Example: Password Rules

Lookaheads can stack at the start of a pattern, each checking one rule without moving forward:

const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
//               has lower    has upper    has digit  8+ chars total

strong.test("password")     // false  (no capital, no digit)
strong.test("Password1")    // true
strong.test("Pass1")        // false  (too short)
false true false
Tip
For user-facing forms, separate checks (one regex per rule) are often better than one giant pattern. You can then tell the user exactly which rule they missed.

// Real Example: Thousands Separators

const withCommas = n => String(n).replace(/\B(?=(\d{3})+$)/g, ",");

withCommas(1234567)   // "1,234,567"

Read it as: "a position inside the number, where the digits after it come in groups of exactly three up to the end." Insert a comma at each such position. (For real apps, toLocaleString() does this for you.)

// Quick Check

What does this log?

console.log("$5 or 5 euros or $50".match(/(?<=\$)\d+/g));
Show answer
["5", "50"]. The lookbehind requires a $ right before the digits, but does not include it in the match.

Why should a zip code check use /^\d{5}$/ instead of /\d{5}/?

  1. It is faster
  2. Without the anchors, any text with 5 digits somewhere inside passes
  3. \d{5} is a SyntaxError without anchors
  4. Anchors make it ignore case
Show answer
B. /\d{5}/ accepts "abc123456xyz". The anchors force the whole text to be exactly 5 digits.

How many characters does (?=px) add to a match?

  1. 0
  2. 1
  3. 2
  4. It depends on the text
Show answer
A. Lookarounds and other assertions match a position. They check what is next without using up any characters.

// Try It

  1. Click Open Playground and run it. Explain the difference between the two results
  2. Count how many times the whole word "the" appears in a paragraph (not "there" or "other")
  3. Extract every number that comes right after a # in "Items #12, #7 and 99"
  4. Add a fourth rule to the password pattern: it must contain one of !@#$
// Video Walkthroughs
Last updated: Oct 10, 2026, 1:47 AM ET