When JavaScript regex can surprise you - a silly bug story
Kamil
Posted on February 15, 2023
Recently one of my customers let me know that they have probably a bug in their app. They want me to look into it - so I jump into “Sherlock, the bug hunter” mode and start snooping!
The misbehaving feature was some kind of search. It takes an input, a Javascript regular expression and as an output returns true if regex was satisfied - and false otherwise.
The feature was pretty simple on the code side - the search function looked similar to the one below:
var search = function(regex, text) {
if (regex && regex.test(text)) {
return true;
}
return false;
}
So we can see that the heart of it was a simple regex.test
call.
The bug report stated that misbehaving example was regex /pony/g
and text this is a pony and not a horse
- we have 100% sure that pony
is in this sentence. To check if everything was working correctly I opened console in Chrome Developer Tools, loaded the search function and check its result:
// Below returns true
search(/pony/g, "this is a pony and not a horse");
// this also returns true!
search(/pony/g, "this is a pony and not a horse");
// ... five runs later
// eh? still returns true!
search(/pony/g, "this is a pony and not a horse");
What is going on?! Every run of this function returns the correct and expected value.
But wait… the regex
and text
was passed using variables from somewhere else. Let’s put them in the variables and try to run the function multiple times again:
var regex = /pony/g
var text = "this is a pony and not a horse";
search(regex, pony); // returns true as expected
search(regex, pony); // returns false :O
search(regex, pony); // returns true - what a trickery!
search(regex, pony); // returns false - wut?!
And now we have a problem - despite the fact we should get true
every time we sometimes get false
instead.
The answer to this weird behaviour is in the… documentation. Quick look at the RegExp.prototype.test() on mdn web docsand we have found the culprit:
JavaScript RegExp objects are stateful when they have the global or sticky flags set (e.g.,
/foo/g
or/foo/y
). They store alastIndex
from the previous match
And the latIndex
property is described as:
The
lastIndex
data property of a RegExp instance specifies the index at which to start the next match.
We have the global option in our regex - and that was the reason that it behaves in a not expected manner. Similar results will be if our test text were this is a pony and second pony - it will give us true
for the first and the second search (as we have two ponies!) and false
for the third one.
The solution for this issue was simply to reset the lastIndex
property of regex if something was found
var search = function(regex, text) {
if (regex && regex.test(text)) {
// reset lastIndex property to make sure
// that next test will start from beginning
regex.lastIndex = 0;
return true;
}
return false;
}
That’s all folks! One line, resetting one property, is all that was needed to fix the issue.
What is the takeaway from this issue? When in doubt and especially when working with regexes - read documentation. It can save you a lot of time. :-)
Posted on February 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.