Testing a regular expression is the fastest way to understand one. A pattern that looks correct on paper routinely matches more than intended, and the only reliable way to find out is to run it against text that includes the cases you expect to fail as well as the ones you expect to pass.

Whether you test regex online or in a unit test, the habit that matters is less about the tool than about the inputs. Most regex bugs in production are not patterns that failed to match; they are patterns that matched something they should have rejected.

Flags change the meaning of a pattern

The regular expression g flag, or global flag, makes a search continue past the first match, which matters for replacement and for counting. The case-insensitive flag removes the distinction between upper and lower case. Neither changes what the pattern describes, only how the engine applies it.

Two others do change meaning. The multiline flag makes the anchors ^ and $ match at every line boundary rather than only at the start and end of the input. The dot-all flag makes . match newline characters, which it otherwise never does. Setting these without intending to is a common source of over-matching.

The global flag has a stateful trap

In JavaScript, a regular expression object with the global flag keeps a lastIndex property between calls. Calling test() repeatedly on the same object therefore resumes from where the previous call stopped, producing an alternating pattern of true and false results on identical input.

This surprises almost everyone once. Avoid it by creating the expression inside the function that uses it, by resetting lastIndex explicitly, or by using a method that does not carry state. It is also why a pattern can behave differently in a tester, which typically evaluates each run fresh, than in a loop in application code.

Anchors are what stop over-matching

An unanchored pattern searches anywhere in the string. A validation expression for a postcode or an identifier that lacks ^ and $ will happily accept a valid value buried inside a longer hostile string, which is a genuine security weakness rather than a cosmetic issue.

Anchor every pattern used for validation. Be aware that in some engines $ also matches immediately before a trailing newline, so a value ending in a line break can pass a check that appears strict. Where an engine offers an absolute end-of-input anchor such as \z, prefer it for validation.

Flavours differ more than people expect

A pattern is not portable by default. JavaScript, Python, Java, PHP, .NET, and the shell tools all implement different dialects. Lookbehind was unavailable in JavaScript for years, named groups use several syntaxes, and PHP requires delimiters around the pattern itself.

The shell is the sharpest edge. Basic grep uses POSIX basic expressions where + and ? are literal characters, while grep -E uses extended syntax and grep -P enables Perl-compatible expressions where available. Test in the flavour you will deploy to, not in whichever tester is nearest.

Building test cases that find bugs

Start with the values that must match, then deliberately construct the ones that must not. For an email pattern, include a missing at sign, a doubled dot, a trailing space, and a valid address embedded in surrounding text. The last of those catches missing anchors immediately.

Add boundary cases: the empty string, a single character, a very long input, and text containing Unicode. A pattern written against ASCII assumptions often behaves unexpectedly once accented letters appear, because \w and \d may or may not include them depending on the engine and its Unicode mode.

Reading a match result properly

A tester should show you the matched span, the captured groups, and the position of each match. The span matters most, because a pattern frequently matches a shorter or longer region than intended while still reporting success.

Quantifiers are the usual reason. They are greedy by default, so .* consumes as much as possible and then backtracks only enough to let the rest of the pattern succeed. Adding ? makes a quantifier lazy, matching as little as possible. Comparing the two on the same input is the quickest way to see the difference.

Performance is part of correctness

Some patterns take exponential time on inputs that are only slightly longer than the ones you tested. Nested quantifiers such as (a+)+ combined with alternation are the classic shape, and against a non-matching string the engine explores an enormous number of paths before giving up.

Test with realistic input sizes and with strings that nearly match but fail at the end, since that is the worst case. If a pattern will run against user input, treat a slow match as a denial-of-service risk, simplify the expression, and prefer a parser when the structure is genuinely more complex than a regular expression should handle.

Testing against the text you will actually see

Invented sample data hides the problems that matter. Paste real log lines, real API responses, or a genuine export from the system the pattern will run against, because production text carries trailing whitespace, inconsistent separators, and encoding artefacts that handcrafted examples never contain.

Run the pattern over a large sample and read the matches rather than the count. A pattern that reports two hundred matches where you expected two hundred can still be matching the wrong spans, and the only way to notice is to look at what it captured. When the input contains credentials or personal data, use a tester that runs entirely in the browser rather than one that uploads what you paste.

References: MDN’s RegExp reference documents the JavaScript flavour, and the Python re documentation shows where another common flavour differs.