Siddarth Narayanan
|March 17, 2026
As I was building Tondova, I stumbled upon a regex bug that caught me by surprise. Consider the following regular expression, which is designed to match phone numbers of the form XXX-XXX-XXXX:
Can you spot the bug?
Well, the issue isn't in the regular expression itself, but in the modifiers applied to it. In JavaScript, we can add modifiers to regular expressions by placing characters after the expression definition /.../. Common modifiers include:
g: Matches all occurrences in a stringi: Enables case-insensitive matchingm: Enables multi-line matchings: Allows . to match newline charactersSo, the problem stems from our usage of the global g modifier in the expression.
Have you ever wondered: how do regular expressions actually match all occurrences of a pattern in a string? No? Well, after encountering this bug, I wanted to understand why. It turns out that when we add the g modifier to a regular expression, we are implicitly turning it into a stateful object. If we take a look at NodeJS' RegExp interface:
We can see that the object maintains a lastIndex property. This property indicates the index where the previous match ended, which determines where the next search begins. To find multiple matches, the engine repeatedly searches the string starting from lastIndex until no more matches are found.
Now we can finally understand why our expression to match phone numbers above was failing. The issue arises because we added the g modifier, which makes the regex stateful. Since the regex is defined in the global scope (i.e., outside the validateNumber function), its lastIndex property is shared across function calls. As a result, each call mutates external state, making the function impure and non-deterministic.
Specifically, we would observe the following behavior:
lastIndex is set to 12 (the length of the input).lastIndex resets to 0.lastIndex was reset.The simplest fix is to remove the g modifier. Notice how we are already anchoring our regular expression to the start and end of the string using ^ and $. This means that the global modifier serves no purpose. Since we only care whether the entire string matches the pattern, we only expect a single match.
We can also inline the regular expression within the function itself.
This ensures that any internal regular expression state is rebuilt on every function call, maintaining the purity of the function.
I hope you found this debugging process interesting! I definitely learned a little more about how regular expressions in JavaScript work.