How to Use the Sticky Flag "y" in JavaScript Regular Expressions
Regular expressions in JavaScript support several flags that alter how pattern matching works. While the g (global) flag is widely known, the y (sticky) flag remains underused and often misunderstood. The sticky flag forces the regex engine to match only at the exact position indicated by the lastIndex property, rather than scanning ahead through the string.
This behavior makes the sticky flag a powerful tool for building tokenizers, lexers, and parsers where you need precise positional control over pattern matching. In this guide, you will learn what sticky mode does, how to search at specific positions, and how y differs from g in practical scenarios.
What Is Sticky Mode?
The sticky flag (y) changes one fundamental aspect of how a regular expression searches a string: instead of scanning forward to find a match anywhere from lastIndex onward, it demands that the match must begin exactly at lastIndex.
Basic Syntax
You add the y flag to a regex just like any other flag:
// Literal syntax
const regex = /hello/y;
// Constructor syntax
const regex2 = new RegExp('hello', 'y');
console.log(regex.sticky); // true
console.log(regex.flags); // "y"
How Sticky Differs from a Normal Search
With no flags (or with the g flag), the regex engine starts at lastIndex and scans forward through the string until it finds a match somewhere. With the y flag, there is no scanning. The match either starts exactly at lastIndex, or it fails immediately.
const str = 'I like cats and cats like me';
// Without sticky (finds "cats" anywhere in the string)
const normalRegex = /cats/;
console.log(normalRegex.exec(str));
// ["cats"] (found at index 7)
// With sticky (must match at position 0 (default lastIndex))
const stickyRegex = /cats/y;
console.log(stickyRegex.exec(str));
// null ("cats" is not at position 0)
The sticky regex returned null because the string starts with "I", not "cats". The engine did not scan forward to find "cats" at index 7. It checked only position 0 and failed.
Setting lastIndex to Control the Starting Position
You can manually set lastIndex to tell the sticky regex where to look:
const str = 'I like cats and cats like me';
const stickyRegex = /cats/y;
stickyRegex.lastIndex = 7;
console.log(stickyRegex.exec(str));
// ["cats"] (found at exactly position 7)
console.log(stickyRegex.lastIndex);
// 11 (updated to the position after the match)
After a successful match, lastIndex is automatically updated to the position immediately after the match, just like with the g flag.
What Happens on Failure
When a sticky regex fails to match at the current lastIndex, it resets lastIndex back to 0:
const regex = /cats/y;
const str = 'I like cats and cats like me';
regex.lastIndex = 5;
console.log(regex.exec(str)); // null (no "cats" at position 5)
console.log(regex.lastIndex); // 0 (reset after failure)
The sticky flag essentially tells the regex engine: "Do not search. Only check if there is a match right here." This is fundamentally different from both unflagged regexes (which search from position 0) and global regexes (which search from lastIndex forward).
Searching at a Specific Position
One of the most practical uses of the sticky flag is matching at a specific position in a string without needing to slice the string or use anchors.
The Problem Without Sticky
Suppose you want to check if a certain pattern exists at a specific index in a string. Without the y flag, you might try approaches like this:
Approach 1: Slicing the string
const str = 'price: 42 dollars';
const sliced = str.slice(7); // "42 dollars"
const match = /\d+/.exec(sliced);
console.log(match[0]); // "42"
// But the index is relative to the sliced string, not the original
console.log(match.index); // 0 (not 7)
This works, but you lose the original position information. You also create a new string on every call, which is wasteful when parsing long inputs.
Approach 2: Using a global regex and setting lastIndex
const str = 'price: 42 dollars';
const regex = /\d+/g;
regex.lastIndex = 7;
const match = regex.exec(str);
console.log(match[0]); // "42"
console.log(match.index); // 7
This seems to work, but it has a subtle problem. Watch what happens if the number is not at position 7:
const str = 'price: xx 42 dollars';
const regex = /\d+/g;
regex.lastIndex = 7;
const match = regex.exec(str);
console.log(match[0]); // "42"
console.log(match.index); // 10 (NOT 7!)
The global regex found "42", but it was at position 10, not 7. The g flag scanned forward past position 7 and found the match elsewhere. This is not what you want when you need a match at a specific position.
The Solution: Sticky Flag
const str = 'price: xx 42 dollars';
const regex = /\d+/y;
regex.lastIndex = 7;
const match = regex.exec(str);
console.log(match); // null (no digits at position 7)
The sticky flag correctly returns null because there are no digits at position 7. It does not scan forward. Now with the correct position:
const str = 'price: 42 dollars';
const regex = /\d+/y;
regex.lastIndex = 7;
const match = regex.exec(str);
console.log(match[0]); // "42"
console.log(match.index); // 7 (exactly where we expected)
Building a Simple Tokenizer
The sticky flag truly shines when building tokenizers or lexers. A tokenizer reads a string from left to right, consuming one token at a time. Each regex must match exactly where the previous match ended.
function tokenize(input) {
const tokens = [];
const patterns = [
{ type: 'NUMBER', regex: /\d+(\.\d+)?/y },
{ type: 'OPERATOR', regex: /[+\-*/]/y },
{ type: 'PAREN_OPEN', regex: /\(/y },
{ type: 'PAREN_CLOSE', regex: /\)/y },
{ type: 'WHITESPACE', regex: /\s+/y },
];
let position = 0;
while (position < input.length) {
let matched = false;
for (const pattern of patterns) {
pattern.regex.lastIndex = position;
const match = pattern.regex.exec(input);
if (match) {
if (pattern.type !== 'WHITESPACE') {
tokens.push({
type: pattern.type,
value: match[0],
position: position
});
}
position = pattern.regex.lastIndex;
matched = true;
break;
}
}
if (!matched) {
throw new Error(`Unexpected character "${input[position]}" at position ${position}`);
}
}
return tokens;
}
const result = tokenize('(3 + 42) * 5.5');
console.log(result);
Output:
[
{ type: 'PAREN_OPEN', value: '(', position: 0 },
{ type: 'NUMBER', value: '3', position: 1 },
{ type: 'OPERATOR', value: '+', position: 3 },
{ type: 'NUMBER', value: '42', position: 5 },
{ type: 'PAREN_CLOSE', value: ')', position: 7 },
{ type: 'OPERATOR', value: '*', position: 9 },
{ type: 'NUMBER', value: '5.5', position: 11 }
]
Each pattern uses the sticky flag, so it only matches if the token starts exactly at the current position. If you used the g flag instead, patterns could accidentally skip over invalid characters and match something further ahead, producing incorrect results.
The sticky flag is the ideal choice for sequential parsing. Whenever you need to consume a string token by token from left to right, use y to ensure each match starts exactly where the last one ended.
Validating Format at a Specific Position
Another practical use is validating that a specific format appears at an expected position within a larger string:
function expectDateAt(str, position) {
const dateRegex = /\d{4}-\d{2}-\d{2}/y;
dateRegex.lastIndex = position;
const match = dateRegex.exec(str);
if (match && match.index === position) {
return match[0];
}
return null;
}
const logLine = '[INFO] 2024-03-15 Server started';
console.log(expectDateAt(logLine, 7)); // "2024-03-15"
console.log(expectDateAt(logLine, 0)); // null (no date at position 0)
console.log(expectDateAt(logLine, 10)); // null (no date starting at position 10)
y vs. g Flag: Differences in exec()
The y and g flags have similar mechanics. Both update lastIndex after each match, and both reset lastIndex to 0 on failure. However, they differ in one critical way: what happens when there is no match at the current lastIndex.
The Core Difference
| Behavior | g (global) | y (sticky) |
|---|---|---|
| Starting position | Begins searching from lastIndex | Must match at lastIndex |
No match at lastIndex | Scans forward through the string | Returns null immediately |
| After successful match | lastIndex = end of match | lastIndex = end of match |
| After failed match | lastIndex = 0 | lastIndex = 0 |
Visual Comparison
const str = 'aaa bbb aaa bbb';
// Global flag (scans forward)
const globalRegex = /bbb/g;
globalRegex.lastIndex = 0;
let match = globalRegex.exec(str);
console.log(`g: found "${match[0]}" at index ${match.index}`);
// g: found "bbb" at index 4
// Started at 0, scanned forward to 4
// Sticky flag (no scanning)
const stickyRegex = /bbb/y;
stickyRegex.lastIndex = 0;
match = stickyRegex.exec(str);
console.log(`y: ${match}`);
// y: null
// Must match at position 0, but "aaa" is there, not "bbb"
The global regex successfully found "bbb" because it scanned forward from position 0. The sticky regex failed because "bbb" does not start at position 0.
Iterating Through Matches
Both flags can be used to iterate through all matches in a string, but with very different results:
Using g to find all occurrences:
const str = 'cat bat cat rat';
const globalRegex = /cat/g;
let match;
while ((match = globalRegex.exec(str)) !== null) {
console.log(`Found "${match[0]}" at index ${match.index}`);
}
Output:
Found "cat" at index 0
Found "cat" at index 8
The g flag skips over non-matching characters (" bat ") and finds the next occurrence.
Using y to find consecutive matches:
const str = 'catcatcat bat cat';
const stickyRegex = /cat/y;
let match;
while ((match = stickyRegex.exec(str)) !== null) {
console.log(`Found "${match[0]}" at index ${match.index}`);
}
Output:
Found "cat" at index 0
Found "cat" at index 3
Found "cat" at index 6
The sticky regex found three consecutive "cat" matches. It stopped at index 9 because position 9 has a space, not "cat". It did not find the "cat" at index 13 because it never scanned forward.
Combining g and y
You can use both flags together (gy), though this is uncommon. When combined, the behavior matches that of the y flag alone, since sticky takes precedence over global in terms of the "no scanning forward" behavior. The g flag's effect is essentially overridden.
const regex = /\d+/gy;
const str = '123abc456';
regex.lastIndex = 0;
console.log(regex.exec(str)); // ["123"] at index 0
// lastIndex is now 3
console.log(regex.exec(str)); // null ("abc" at position 3, not digits)
console.log(regex.lastIndex); // 0 (reset after failure)
In most cases, using y alone is clearer than combining gy. The combination does not add functionality. Use y when you need sticky behavior.
Sticky Flag with str.match()
The match() method behaves differently with the y flag compared to g:
const str = 'aaa bbb aaa';
// With g flag: match() returns all matches as an array
console.log(str.match(/aaa/g));
// ["aaa", "aaa"]
// With y flag: match() behaves like exec() (single match)
console.log(str.match(/aaa/y));
// ["aaa"] (only the first match, with index and groups)
With the y flag, str.match() does not return all matches as an array like it does with g. It returns a single match result, similar to calling exec() once.
Sticky Flag with str.matchAll()
The matchAll() method requires either the g or y flag (or it throws an error). When used with y, it produces consecutive matches:
const str = '112233 44';
const regex = /\d\d/y;
const matches = [...str.matchAll(regex)];
for (const m of matches) {
console.log(`"${m[0]}" at index ${m.index}`);
}
Output:
"11" at index 0
"22" at index 2
"33" at index 4
The iteration stopped at index 6 because position 6 is a space, and the sticky regex cannot scan past it.
Sticky Flag with str.replace() and str.replaceAll()
const str = 'aaabbbccc';
// Replace consecutive 'a' characters at the start
const result = str.replace(/a/y, 'X');
console.log(result);
// "Xaabbbccc" (replaced only the first match at position 0)
// replaceAll with y flag: replaces consecutive matches
const result2 = str.replaceAll(/a/gy, 'X');
console.log(result2);
// "XXXbbbccc" (replaced all consecutive 'a' characters)
str.replaceAll() requires the g flag. To use it with sticky behavior, you must combine both: /pattern/gy. Using y alone with replaceAll() throws a TypeError.
Sticky Flag with str.search()
The search() method ignores the y flag (and the g flag). It always searches from the beginning of the string:
const str = 'hello world';
const regex = /world/y;
regex.lastIndex = 6;
// search() ignores lastIndex and the y flag
console.log(str.search(regex)); // 6 (found, but not because of sticky)
Since search() does not respect lastIndex or the sticky flag, it is not useful in combination with y.
Sticky Flag with str.split()
The split() method also ignores the sticky flag:
const str = 'one-two-three';
console.log(str.split(/-/y));
// ["one", "two", "three"] (same as without y)
The behavior is identical to using the regex without any flags. The y flag has no practical effect on split().
Practical Example: Parsing Key-Value Pairs
Here is a more complete example of using the sticky flag to parse a structured string:
function parseKeyValuePairs(input) {
const result = {};
const keyRegex = /([a-zA-Z_]\w*)\s*=\s*/y;
const stringValueRegex = /"([^"]*)"\s*;?\s*/y;
const numberValueRegex = /(\d+(?:\.\d+)?)\s*;?\s*/y;
const boolValueRegex = /(true|false)\s*;?\s*/y;
let position = 0;
// Skip leading whitespace
const wsRegex = /\s*/y;
wsRegex.lastIndex = position;
wsRegex.exec(input);
position = wsRegex.lastIndex;
while (position < input.length) {
// Match key
keyRegex.lastIndex = position;
const keyMatch = keyRegex.exec(input);
if (!keyMatch) break;
const key = keyMatch[1];
position = keyRegex.lastIndex;
// Try to match value (string, number, or boolean)
let value;
stringValueRegex.lastIndex = position;
let valueMatch = stringValueRegex.exec(input);
if (valueMatch) {
value = valueMatch[1];
position = stringValueRegex.lastIndex;
} else {
numberValueRegex.lastIndex = position;
valueMatch = numberValueRegex.exec(input);
if (valueMatch) {
value = parseFloat(valueMatch[1]);
position = numberValueRegex.lastIndex;
} else {
boolValueRegex.lastIndex = position;
valueMatch = boolValueRegex.exec(input);
if (valueMatch) {
value = valueMatch[1] === 'true';
position = boolValueRegex.lastIndex;
} else {
throw new Error(`Unexpected value at position ${position}`);
}
}
}
result[key] = value;
}
return result;
}
const config = `
name = "Alice";
age = 30;
score = 99.5;
active = true;
`;
console.log(parseKeyValuePairs(config));
Output:
{
name: "Alice",
age: 30,
score: 99.5,
active: true
}
Every regex uses the y flag, ensuring each token is matched at exactly the expected position. If any token is malformed or out of order, the parser immediately detects it instead of silently skipping ahead.
Summary
| Feature | Sticky (y) | Global (g) |
|---|---|---|
| Match position | Exactly at lastIndex | From lastIndex forward |
| Scans ahead | No | Yes |
Updates lastIndex on success | Yes | Yes |
Resets lastIndex on failure | Yes (to 0) | Yes (to 0) |
| Best for | Tokenizers, positional parsing | Finding all matches anywhere |
str.match() behavior | Single match (like exec) | Array of all matches |
str.matchAll() | Consecutive matches only | All matches in string |
The sticky flag gives you exact positional control over regex matching. Use it when you need to parse structured input sequentially and want guarantees that each pattern matches exactly where you expect, with no silent skipping.