Skip to main content

How to Use Anchors in JavaScript Regular Expressions

Regular expressions do not only match characters. They also match positions within a string. Anchors are special regex tokens that assert a position without consuming any characters. The two most fundamental anchors, ^ and $, let you specify that a pattern must appear at the beginning or end of a string, turning a general search into a precise positional requirement.

Without anchors, a regex like /\d+/ finds digits anywhere in a string. With anchors, /^\d+$/ requires the entire string to consist of digits. This distinction is the foundation of input validation, line-by-line processing, and countless other patterns. This guide covers how ^ and $ work, how to use them together for full-string matching, and how the m flag changes their behavior for multiline text.

^: Start of String

The caret ^ anchor asserts that the match must begin at the start of the string. It does not match any character. It asserts a position.

const regex = /^Hello/;

console.log(regex.test('Hello World')); // true (starts with "Hello")
console.log(regex.test('Say Hello')); // false ("Hello" is not at the start)
console.log(regex.test('hello World')); // false (case mismatch)
console.log(regex.test('Hello')); // true
console.log(regex.test('')); // false (empty string has no "Hello")

The ^ anchor matches the position before the first character. The pattern after ^ describes what must appear at that position.

Combining ^ with Other Patterns

The ^ anchor works with any regex pattern that follows it:

// Starts with a digit
console.log(/^\d/.test('3 cats')); // true
console.log(/^\d/.test('cats')); // false

// Starts with one or more whitespace characters
console.log(/^\s+/.test(' hello')); // true
console.log(/^\s+/.test('hello')); // false

// Starts with a specific word
console.log(/^Error:/.test('Error: file not found')); // true
console.log(/^Error:/.test('Warning: low memory')); // false

// Starts with a capital letter
console.log(/^[A-Z]/.test('Hello')); // true
console.log(/^[A-Z]/.test('hello')); // false

// Starts with a protocol
console.log(/^https?:\/\//.test('https://example.com')); // true
console.log(/^https?:\/\//.test('ftp://example.com')); // false

Using ^ with match()

When used with match(), the ^ anchor constrains where the match can start, but the rest of the pattern can match additional characters:

const text = 'Hello World';

// Without ^: finds "World" anywhere
console.log(text.match(/\w+$/)); // ["World"]

// With ^: pattern must start at the beginning
console.log(text.match(/^\w+/)); // ["Hello"]
// Matches "Hello": the first sequence of word characters from the start

const logLine = '2024-03-15 08:30:22 INFO Server started';

// Extract the date at the start
const date = logLine.match(/^\d{4}-\d{2}-\d{2}/);
console.log(date[0]); // "2024-03-15"

// Extract everything from the start up to the first space
const firstToken = logLine.match(/^\S+/);
console.log(firstToken[0]); // "2024-03-15"

^ Inside Character Classes

Inside square brackets [...], the caret has a completely different meaning: it negates the character class. This is a common source of confusion:

// ^ as an anchor: start of string
/^abc/ // String must start with "abc"

// ^ inside [...]: negation of character class
/[^abc]/ // Any character that is NOT a, b, or c

console.log(/^abc/.test('abcdef')); // true (starts with "abc")
console.log(/[^abc]/.test('d')); // true (d" is not a, b, or c)
console.log(/[^abc]/.test('a')); // false (a" is in the negated set)

The position of ^ determines its meaning: at the start of a pattern (or after | for alternation), it is an anchor. Inside [...] right after the opening bracket, it negates the class.

$: End of String

The dollar sign $ anchor asserts that the match must end at the end of the string. Like ^, it matches a position, not a character.

const regex = /World$/;

console.log(regex.test('Hello World')); // true (ends with "World")
console.log(regex.test('World Hello')); // false ("World" is not at the end)
console.log(regex.test('World')); // true
console.log(regex.test('Hello World!')); // false (ends with "!" not "World")

Common Uses of $

// Ends with a file extension
console.log(/\.js$/.test('app.js')); // true
console.log(/\.js$/.test('app.js.map')); // false
console.log(/\.js$/.test('app.jsx')); // false

// Ends with a period (sentence ending)
console.log(/\.$/.test('Hello world.')); // true
console.log(/\.$/.test('Hello world!')); // false

// Ends with a digit
console.log(/\d$/.test('item3')); // true
console.log(/\d$/.test('item3a')); // false

// Ends with specific text
console.log(/bye$/i.test('Goodbye')); // true
console.log(/bye$/i.test('Good bye')); // true
console.log(/bye$/i.test('bye bye')); // true

Using $ with match()

const filename = 'document.backup.pdf';

// Extract the file extension at the end
const ext = filename.match(/\.\w+$/);
console.log(ext[0]); // ".pdf"

// Extract the last word
const text = 'The quick brown fox';
const lastWord = text.match(/\w+$/);
console.log(lastWord[0]); // "fox"

// Extract trailing whitespace
const padded = 'Hello ';
const trailing = padded.match(/\s+$/);
console.log(trailing[0].length); // 3 spaces

// Check for trailing newline
console.log(/\n$/.test('Hello\n')); // true
console.log(/\n$/.test('Hello')); // false

Using $ with replace()

The $ anchor is useful for modifying the end of strings:

// Add a period if the string doesn't end with one
function ensureEndsPeriod(text) {
return /\.$/.test(text) ? text : text + '.';
}

console.log(ensureEndsPeriod('Hello world')); // "Hello world."
console.log(ensureEndsPeriod('Hello world.')); // "Hello world."

// Remove trailing slashes from a URL
function removeTrailingSlash(url) {
return url.replace(/\/+$/, '');
}

console.log(removeTrailingSlash('https://example.com/')); // "https://example.com"
console.log(removeTrailingSlash('https://example.com///')); // "https://example.com"
console.log(removeTrailingSlash('https://example.com')); // "https://example.com"

// Remove trailing whitespace
function trimEnd(str) {
return str.replace(/\s+$/, '');
}

console.log(trimEnd('Hello ')); // "Hello"

Testing for Full Match

The most powerful use of anchors is combining ^ and $ together to require that the entire string matches the pattern. Without both anchors, a regex can match any substring. With both, it must match the whole input from start to finish.

The Difference

const text = 'abc123def';

// Without anchors: finds digits anywhere in the string
console.log(/\d+/.test(text)); // true (matches "123")

// With ^ only: digits must start at the beginning
console.log(/^\d+/.test(text)); // false (starts with "abc")

// With $ only: digits must reach the end
console.log(/\d+$/.test(text)); // false (ends with "def")

// With both ^ and $: entire string must be digits
console.log(/^\d+$/.test(text)); // false

console.log(/^\d+$/.test('123')); // true (entire string is digits)
console.log(/^\d+$/.test('')); // false (+ requires at least one digit)

Input Validation Patterns

Full-match validation is the most common use of ^...$. Here are practical patterns:

// Validate an integer (positive)
function isPositiveInteger(str) {
return /^[1-9]\d*$/.test(str);
}

console.log(isPositiveInteger('42')); // true
console.log(isPositiveInteger('0')); // false (doesn't start with 1-9)
console.log(isPositiveInteger('042')); // false (leading zero)
console.log(isPositiveInteger('-5')); // false
console.log(isPositiveInteger('3.14')); // false

// Validate an integer (positive or negative, including zero)
function isInteger(str) {
return /^-?(0|[1-9]\d*)$/.test(str);
}

console.log(isInteger('0')); // true
console.log(isInteger('42')); // true
console.log(isInteger('-7')); // true
console.log(isInteger('007')); // false (leading zeros)
console.log(isInteger('3.14')); // false

// Validate a decimal number
function isDecimal(str) {
return /^-?(0|[1-9]\d*)(\.\d+)?$/.test(str);
}

console.log(isDecimal('3.14')); // true
console.log(isDecimal('-0.5')); // true
console.log(isDecimal('42')); // true (integer is valid decimal)
console.log(isDecimal('.5')); // false (no leading digit)
console.log(isDecimal('3.')); // false (no digits after dot)
// Validate a hex color code
function isHexColor(str) {
return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(str);
}

console.log(isHexColor('#fff')); // true
console.log(isHexColor('#FF5733')); // true
console.log(isHexColor('#1234')); // false (4 digits, not 3 or 6)
console.log(isHexColor('FF5733')); // false (missing #)
console.log(isHexColor('#GGGGGG')); // false (invalid hex)

// Validate an email (simplified)
function isEmail(str) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}

console.log(isEmail('user@example.com')); // true
console.log(isEmail('user@.com')); // false
console.log(isEmail('user example.com')); // false
console.log(isEmail('@example.com')); // false

// Validate a date format (YYYY-MM-DD)
function isDateFormat(str) {
return /^\d{4}-\d{2}-\d{2}$/.test(str);
}

console.log(isDateFormat('2024-03-15')); // true
console.log(isDateFormat('24-3-15')); // false
console.log(isDateFormat('2024/03/15')); // false
// Validate a URL slug (lowercase letters, digits, hyphens)
function isValidSlug(str) {
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(str);
}

console.log(isValidSlug('hello-world')); // true
console.log(isValidSlug('my-blog-post')); // true
console.log(isValidSlug('hello')); // true
console.log(isValidSlug('Hello-World')); // false (uppercase)
console.log(isValidSlug('-hello')); // false (starts with hyphen)
console.log(isValidSlug('hello-')); // false (ends with hyphen)
console.log(isValidSlug('hello--world')); // false (double hyphen)

// Validate an IP address (IPv4)
function isIPv4(str) {
const octet = '(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])';
const regex = new RegExp(`^${octet}\\.${octet}\\.${octet}\\.${octet}$`);
return regex.test(str);
}

console.log(isIPv4('192.168.1.1')); // true
console.log(isIPv4('0.0.0.0')); // true
console.log(isIPv4('255.255.255.255'));// true
console.log(isIPv4('256.1.1.1')); // false
console.log(isIPv4('1.2.3')); // false
console.log(isIPv4('1.2.3.4.5')); // false

Why Full Match Matters for Validation

Without both anchors, validation patterns match substrings and produce false positives:

// ❌ Without anchors: matches the digit substring "3" inside "abc3def"
console.log(/\d+/.test('abc3def')); // true (not what we want!)

// ❌ With only ^: matches "3" at position 3, accepts "3def"
console.log(/^\d+/.test('3def')); // true (still wrong!)

// ❌ With only $: matches "3" at the end, accepts "abc3"
console.log(/\d+$/.test('abc3')); // true (also wrong!)

// ✅ With both: requires ENTIRE string to be digits
console.log(/^\d+$/.test('abc3def')); // false (correct!)
console.log(/^\d+$/.test('3def')); // false (correct!)
console.log(/^\d+$/.test('abc3')); // false (correct!)
console.log(/^\d+$/.test('123')); // true (correct!)
warning

Always use both ^ and $ when validating that an entire input matches a format. Using only one anchor or no anchors at all is the most common cause of validation bypasses. A regex like /\d{3}/ does not validate a three-digit string. It validates that the string contains three consecutive digits, which is a very different check.

Empty String Considerations

Be mindful of how your anchored pattern handles empty strings:

// ^ followed by $ with nothing in between: matches empty string
console.log(/^$/.test('')); // true
console.log(/^$/.test(' ')); // false

// Using * (zero or more): allows empty string
console.log(/^\d*$/.test('')); // true (zero digits is valid)
console.log(/^\d*$/.test('123')); // true

// Using + (one or more): requires at least one character
console.log(/^\d+$/.test('')); // false (needs at least one digit)
console.log(/^\d+$/.test('123')); // true

// Making empty input explicitly invalid
function isNonEmptyDigits(str) {
return /^\d+$/.test(str); // + ensures at least one digit
}

// Or allowing empty with explicit length check
function isOptionalDigits(str) {
return str === '' || /^\d+$/.test(str);
}

Anchors with Alternation

When combining anchors with the | (OR) operator, be careful about grouping. The | has low precedence, so anchors may not apply to all alternatives:

// ❌ Common mistake: ^ and $ only apply to the nearest alternative
const wrong = /^yes|no$/;
// This means: (^yes) OR (no$)
// Matches "yes" at the start OR "no" at the end

console.log(wrong.test('yes')); // true
console.log(wrong.test('yes please')); // true ("yes" at start matches!)
console.log(wrong.test('say no')); // true ("no" at end matches!)

// ✅ Correct: group the alternatives
const correct = /^(yes|no)$/;
// Matches "yes" or "no" as the entire string

console.log(correct.test('yes')); // true
console.log(correct.test('no')); // true
console.log(correct.test('yes please')); // false
console.log(correct.test('say no')); // false

This is a very common mistake. When using alternation with anchors, always wrap the alternatives in a group:

// ❌ Anchors don't cover all alternatives
/^jpg|png|gif$/ // (^jpg) | (png) | (gif$)

// ✅ Anchors cover the entire group
/^(jpg|png|gif)$/ // ^(jpg|png|gif)$

// ✅ Non-capturing group if you don't need the capture
/^(?:jpg|png|gif)$/

Multiline Mode m: ^/$ at Line Boundaries

By default, ^ matches only the very start of the string and $ matches only the very end. The m (multiline) flag changes this behavior: ^ also matches after every newline character, and $ also matches before every newline character. This lets you process text line by line with a single regex.

Default Behavior (Without m)

const text = `First line
Second line
Third line`;

// ^ only matches at the very start of the string
console.log(text.match(/^\w+/g));
// ["First"] (only the first line's first word)

// $ only matches at the very end of the string
console.log(text.match(/\w+$/g));
// ["line"] (only the last line's last word)

With the m Flag

const text = `First line
Second line
Third line`;

// With m: ^ matches at the start of each line
console.log(text.match(/^\w+/gm));
// ["First", "Second", "Third"]

// With m: $ matches at the end of each line
console.log(text.match(/\w+$/gm));
// ["line", "line", "line"]

// Match entire lines
console.log(text.match(/^.+$/gm));
// ["First line", "Second line", "Third line"]

What Counts as a Line Boundary

In multiline mode, ^ matches at positions:

  • The very start of the string
  • Immediately after \n (newline)

And $ matches at positions:

  • The very end of the string
  • Immediately before \n (newline)
const text = 'line1\nline2\nline3';

// ^ matches after each \n
console.log(text.match(/^line\d/gm));
// ["line1", "line2", "line3"]

// $ matches before each \n and at the end
console.log(text.match(/\d$/gm));
// ["1", "2", "3"]
note

The m flag does not change how the dot (.) works. The dot still does not match newlines unless you also use the s flag. The m flag only affects the ^ and $ anchors.

const text = `Hello
World`;

// m does NOT make . match newlines
console.log(text.match(/^.+$/gm));
// ["Hello", "World"] (each line separately, . stops at \n)

// s makes . match newlines (but ^ and $ lose line-level meaning without m)
console.log(text.match(/^.+$/gs));
// ["Hello\nWorld"] (one match spanning both lines)

// Combining m and s: ^ and $ match lines, . crosses newlines
// (less common, specific use cases)

Practical Examples with Multiline Mode

Processing Log Files:

const log = `2024-03-15 08:30:22 INFO Server started
2024-03-15 08:30:23 DEBUG Connection pool initialized
2024-03-15 08:31:01 ERROR Failed to connect to database
2024-03-15 08:31:05 WARN Retrying connection
2024-03-15 08:31:06 INFO Connection established`;

// Find all ERROR lines
const errorLines = log.match(/^.*ERROR.*$/gm);
console.log(errorLines);
// ["2024-03-15 08:31:01 ERROR Failed to connect to database"]

// Find all lines that start with a specific date
const marchLines = log.match(/^2024-03-15.*$/gm);
console.log(marchLines.length); // 5 (all lines)

// Extract timestamps from lines containing "connect" (case-insensitive)
const connectTimes = log.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*connect.*$/gim);
console.log(connectTimes);
// Lines containing "connect" (case-insensitive)

Processing CSV Data:

const csv = `name,age,city
Alice,30,NYC
Bob,25,LA
Charlie,35,Chicago`;

// Skip the header, match data rows
const dataRows = csv.match(/^[A-Z][a-z]+,\d+,\w+$/gm);
console.log(dataRows);
// ["Alice,30,NYC", "Bob,25,LA", "Charlie,35,Chicago"]

// Find rows where age is 30+
const rows = csv.match(/^.+,3\d,.*$/gm);
console.log(rows);
// ["Alice,30,NYC", "Charlie,35,Chicago"]

Commenting and Uncommenting Lines:

const code = `host = localhost
port = 3000
# debug = true
database = mydb
# cache = redis`;

// Remove comment lines (lines starting with #)
const uncommented = code.replace(/^#\s?.*$/gm, '').replace(/\n{2,}/g, '\n').trim();
console.log(uncommented);
// "host = localhost\nport = 3000\ndatabase = mydb"

// Comment out lines containing "database"
const commented = code.replace(/^(.*database.*)$/gm, '# $1');
console.log(commented);
// Lines with "database" now have "# " prepended

Adding Line Numbers:

const text = `First line
Second line
Third line`;

let lineNum = 0;
const numbered = text.replace(/^/gm, () => `${++lineNum}: `);
console.log(numbered);
// "1: First line\n2: Second line\n3: Third line"

Filtering Lines by Pattern:

function grepLines(text, pattern, flags = '') {
const regex = new RegExp(`^.*${pattern}.*$`, 'gm' + flags);
return text.match(regex) || [];
}

const serverLog = `GET /index.html 200
POST /api/users 201
GET /api/data 500
GET /about.html 200
POST /api/login 401`;

// Find all failed requests (status 4xx or 5xx)
console.log(grepLines(serverLog, '[45]\\d{2}$'));
// ["GET /api/data 500", "POST /api/login 401"]

// Find all GET requests
console.log(grepLines(serverLog, '^GET'));
// ["GET /index.html 200", "GET /api/data 500", "GET /about.html 200"]

// Find API routes
console.log(grepLines(serverLog, '/api/'));
// ["POST /api/users 201", "GET /api/data 500", "POST /api/login 401"]

Extracting Sections from Structured Text:

const markdown = `# Introduction
This is the intro paragraph.
It spans multiple lines.

# Installation
Run npm install to get started.

# Usage
Import the module and call init().
Use the API as documented.`;

// Extract all section headers
const headers = markdown.match(/^# .+$/gm);
console.log(headers);
// ["# Introduction", "# Installation", "# Usage"]

// Extract section headers without the "# " prefix
const headerTexts = markdown.match(/^# (.+)$/gm).map(h => h.replace(/^# /, ''));
console.log(headerTexts);
// ["Introduction", "Installation", "Usage"]

Multiline Mode with replace()

The m flag is particularly useful with replace() for transforming text on a per-line basis:

// Trim leading whitespace from each line
const indented = ` Hello
World
JavaScript`;

const trimmed = indented.replace(/^\s+/gm, '');
console.log(trimmed);
// "Hello\nWorld\nJavaScript"

// Indent every line by 4 spaces
const code = `function hello() {
console.log("hi");
}`;

const indentedCode = code.replace(/^/gm, ' ');
console.log(indentedCode);
// " function hello() {\n console.log(\"hi\");\n }"

// Prefix every line with a comment marker
const commented = code.replace(/^/gm, '// ');
console.log(commented);
// "// function hello() {\n// console.log(\"hi\");\n// }"

// Remove empty lines
const withBlanks = `Line 1

Line 2


Line 3`;

const noEmpty = withBlanks.replace(/^\s*$/gm, '').replace(/\n{2,}/g, '\n');
console.log(noEmpty);
// "Line 1\nLine 2\nLine 3"

m Flag with Full-Line Matching

Using ^...$ with the m flag matches each complete line against the pattern:

const data = `valid123
invalid!@#
hello_world
test 123
ALLCAPS`;

// Find lines that contain only word characters
const wordLines = data.match(/^\w+$/gm);
console.log(wordLines);
// ["valid123", "hello_world", "ALLCAPS"]
// "invalid!@#" excluded (has special chars)
// "test 123" excluded (has space)

Summary of Anchor Behavior

PatternWithout mWith m
^Start of string onlyStart of string AND start of each line
$End of string onlyEnd of string AND end of each line
^...$Entire string must matchEach line tested independently
.Unchanged (no newline)Unchanged (no newline). Use s for dotall
const text = 'line1\nline2\nline3';

// Summary of behaviors:
console.log(text.match(/^\w+/g)); // ["line1"] (only string start)
console.log(text.match(/^\w+/gm)); // ["line1", "line2", "line3"] (each line start)

console.log(text.match(/\w+$/g)); // ["line3"] (only string end)
console.log(text.match(/\w+$/gm)); // ["line1", "line2", "line3"] (each line end)

Summary

Anchors control where in a string a pattern can match, without consuming any characters:

  • ^ asserts the start of the string. Use it to ensure a pattern appears at the beginning. Inside character classes [^...], it negates the class instead.
  • $ asserts the end of the string. Use it to ensure a pattern appears at the ending.
  • ^ and $ together (/^pattern$/) require the entire string to match the pattern. This is essential for input validation. Without both anchors, the regex matches substrings and produces false positives.
  • When using alternation (|) with anchors, always wrap alternatives in a group: /^(yes|no)$/ not /^yes|no$/.
  • The m (multiline) flag changes ^ and $ to also match at line boundaries (after and before \n), enabling line-by-line processing of multiline text. The m flag does not affect the dot (.); use the s flag for that.
  • Handle empty strings carefully: /^\d*$/ matches empty strings (zero digits), while /^\d+$/ requires at least one digit.