Most of a regular expression describes characters you want to find. Anchors are different. The caret pattern:^ and the dollar sign pattern:$ don’t match any character at all — they match a position. Think of them as claims about where you are in the string: “we’re at the very beginning” or “we’re at the very end”.
That single idea unlocks a lot of practical validation, so it’s worth getting the mental model right before touching code.
Two positions, not two characters
pattern:^ succeeds only at the start of the text. pattern:$ succeeds only at the end. Neither one consumes a character, which is why they’re called anchors: they pin the rest of your pattern to a fixed spot.
Test whether a string begins with Raj:
let str1 = "Raj brewed a pot of coffee";alert( /^Raj/.test(str1) ); // true
Read pattern:^Raj out loud as “start of string, immediately followed by Raj”. If the string were "and Raj brewed...", this would return false — Raj is there, but not at position zero.
The mirror image checks the ending. Does the string finish with empty?
let str1 = "the kettle was nearly empty";alert( /empty$/.test(str1) ); // true
^Raj … ty$
^ is the gap before the first character · $ is the gap after the last
An anchor marks a boundary between characters, it does not sit on a character of its own.
For these two examples you could reach for the string methods startsWith and endsWith instead, and you probably should — they’re clearer when the test is that simple. Anchors earn their keep once the boundary sits next to a real pattern, like “starts with three digits” or “ends with .png”.
Testing for a full match
The real power move is using both anchors at once. pattern:^...$ wraps the whole pattern, forcing it to describe the entire string from first character to last. This is the standard shape for input validation, where partial matches are exactly what you want to reject.
Check whether a string is a locker code in 24-08 format: two digits, a dash, two more digits. In regex that’s pattern:\d\d-\d\d, and to demand the whole string fits, you anchor both ends:
Walk through why "24-081" fails. The pattern pattern:\d\d-\d\d happily matches the 24-08 portion, but then pattern:$ demands the end of the string right after — and there’s a stray 1 sitting there. The condition breaks, so the test returns false.
“24-08”^24-08$true
“24-081”^24-081$false
The pink 1 is left over, so $ can’t sit right after the match.
With both anchors, the pattern must account for every character. A leftover character after the match fails the $ test.
Any deviation — an extra character, a missing digit, a space at the front — makes the whole test fail. That strictness is the point. When you validate a form field, you don’t want 24-08x or 24-08 sneaking through as valid.
Type into the field below and watch pattern:^\d\d-\d\d$ judge each keystroke live. Try the tricky cases: a trailing letter, a leading space, or a three-digit second block.
const input = document.getElementById("tv-in");
const verdict = document.getElementById("tv-verdict");
const re = /^\d\d-\d\d$/;
function update() {
const value = input.value;
const ok = re.test(value);
verdict.textContent = ok
? 'test("' + value + '") --> true'
: 'test("' + value + '") --> false';
verdict.className = "verdict " + (ok ? "pass" : "fail");
}
input.addEventListener("input", update);
update();
Toggle the two anchors below to feel the difference. The core pattern is always pattern:\d\d-\d\d; the checkboxes decide whether it must sit at the start, the end, both, or neither of the test string.
const str = document.getElementById("ac-str");
const start = document.getElementById("ac-start");
const end = document.getElementById("ac-end");
const patternBox = document.getElementById("ac-pattern");
const verdict = document.getElementById("ac-verdict");
function update() {
const body = "\\d\\d-\\d\\d";
const source = (start.checked ? "^" : "") + body + (end.checked ? "$" : "");
const re = new RegExp(source);
patternBox.textContent = "/" + source + "/";
const ok = re.test(str.value);
verdict.textContent = ok ? "match --> true" : "no match --> false";
verdict.className = "verdict " + (ok ? "pass" : "fail");
}
str.addEventListener("input", update);
start.addEventListener("change", update);
end.addEventListener("change", update);
update();
The zero-width nature
This is easy to see with the match results. Because anchors consume nothing, they contribute nothing to the matched text:
// ^ and $ add no characters to the resultalert( "24-08".match(/^\d\d-\d\d$/)[0] ); // "24-08", not "^24-08$"alert( "24-08".match(/^\d\d-\d\d$/)[0].length ); // 5
The captured string is just the five characters 24-08. The anchors did their job — verifying the boundaries — and then stepped out of the way. That’s the practical meaning of “zero width”: they influence whether a match happens without adding anything to it.
A note on multiline
Everything above assumes the default behavior, where pattern:^ and pattern:$ mean the start and end of the entire string. With the pattern:m (multiline) flag, they change meaning and start matching at the boundaries of each line instead. That’s a separate topic, covered in the next article — just don’t be surprised when the same anchors behave differently once pattern:m is in play.