One of the most known and agreed upon principles in software development is DRY - don’t repeat yourself. In this short post I’ll show you how applying DRY to regular expressions in JavaScript can be tricky. The examples will be written for Node.js, but the idea applies to other environments as well.

Let’s say we want to use regular expressions to check if a string includes digits:

let string = "Hello world! 123";

if (/\d+/.test(string)) {
  console.log("Digit found!");
} else {
  console.log("Digit not found.");
}

Now, just for the sake of argument, assume that this regular expression might get more complex in the future. Also, assume that we’ll need it in mulitple parts of the application. The logical thing to do would be to put it in a module so that it can be reused.

exports.digits = /\d+/g;

Notice that we’ve also added the g flag for “global”. It will allow us to find all digits in a string instead of just the first match. If we’re preparing for reuse we might as well support other use cases.

Now we can open up the Node.js console, import the regex and use it.

const { digits } = require("./regex");

digits.test("Hello world! 123");
// true
digits.test("321");
// false
digits.test("321");
// true

And everything works as expected… Except it doesn’t.

If you’ve noticed the humble false in the example above, you’ve just witnessed a pecularity of the JavaScript RegExp object that caused me to beat my head against the wall for better part of a workday. Figuratively of course.

What happened there? Well, according to MDN Web Docs:

When a regex has the global flag set, test() will advance the lastIndex of the regex. (RegExp.prototype.exec() also advances the lastIndex property.)

Further calls to test(str) will resume searching str starting from lastIndex. The lastIndex property will continue to increase each time test() returns true.

Note: As long as test() returns true, lastIndex will not reset—even when testing a different string!

When test() returns false, the calling regex’s lastIndex property will reset to 0.

Why it works like that will remain a mystery to me. Good thing it’s well documented, but as one of my coworkers said: “just because it’s documented doesn’t mean it’s okay”.

Now what?

I’ve written this in hope that at least one person’s time will be saved after reading.

And as much enjoyable laughing at JavaScript might be for me, it wouldn’t be fair not to suggest a solution.

Although one could reset lastIndex after each call to test():

const { digits } = require("./regex");

digits.test("Hello world! 123");
// true
digits.lastIndex = 0;
digits.test("321");
// true
digits.lastIndex = 0;
digits.test("321");
// true

But then it might be less painful to just copy-paste the regex where needed.

A better solution is to build a habit of using search or match functions of String, when working with global regexes, instead of relying on test.

const { digits } = require("./regex");

"Hello world! 123".search(digits) > -1;
// true
"321".search(digits) > -1;
// true
"321".search(digits) > -1;
// true