How to Test Regular Expressions
A practical approach to writing and testing regular expressions, from basic building blocks to a workflow for verifying patterns against real data.
Regular expressions are one of the most powerful and most misused tools in a developer's kit. A well-tested pattern can replace pages of manual string-parsing logic; a poorly tested one can silently fail on edge cases for months before anyone notices. The difference usually isn't the complexity of the pattern, it's whether it was actually tested against realistic input before shipping.
Start With What You're Actually Matching
Before writing a single character of regex, write down a handful of concrete examples: strings that should match, and, just as important, strings that should not match. This second category is the one people skip, and it's where most regex bugs hide. A pattern intended to match email addresses that was only ever tested against name@example.com will happily also match plenty of things that aren't valid emails, unless you also test it against strings designed to break it.
For example, if you're matching email addresses, your test set might include:
hello@example.com, should matchfirst.last+tag@sub.example.co.uk, should match (dots, plus-addressing, subdomains)not-an-email, should not match@example.com, should not match (missing local part)hello@example, arguably should not match (no top-level domain), depending on how strict you need to be
The Building Blocks
A small set of regex concepts covers the vast majority of real-world patterns:
- Literal characters match themselves:
catmatches the text "cat". - Character classes (
[abc],[a-z],[^0-9]) match any one character from a set, or, with^, any character not in the set. - Quantifiers (
*,+,?,{2,4}) control how many times the preceding element can repeat: zero or more, one or more, zero or one, or a specific range. - Anchors (
^,$) match the start and end of a line (or string, depending on the multiline flag) rather than matching any character. - Groups (
(...)) both group parts of a pattern together and capture the matched text for later use. - Alternation (
a|b) matches either the pattern on the left or the pattern on the right. - Escapes (
\d,\w,\s) are shorthand for common character classes: digits, word characters, and whitespace, respectively.
Most real patterns are combinations of these: \b[\w.-]+@[\w.-]+\.\w+\b (a simplified email pattern) is just character classes, quantifiers, and literal characters chained together with word boundary anchors (\b) at each end.
Building a Pattern Incrementally
Trying to write a complex pattern in one shot and testing it only at the end is a recipe for confusion when it doesn't work, you won't know which part is wrong. A better approach:
- Match the simplest possible case first. For an email pattern, start with something crude like
.+@.+and confirm it matches your basic example. - Add one constraint at a time. Replace
.+on the left with a proper character class for valid local-part characters, test again, then move to the domain side. - Test against your "should not match" list after every change. This is the step that catches over-permissive patterns before they ship.
- Add anchors last, once the core pattern works, since anchors change how the pattern behaves against partial strings and can mask bugs in the core pattern if added too early.
Flags Change Behavior More Than People Expect
The same pattern can behave very differently depending on which flags are active:
- Global (
g) finds all matches in a string instead of stopping after the first one. Forgetting this flag is a common cause of "my regex only replaced the first occurrence" bugs. - Case-insensitive (
i) makes letter matching ignore case entirely. - Multiline (
m) changes^and$to match the start and end of each line within a multi-line string, rather than only the start and end of the whole string. - Dot-all (
s) makes.match newline characters too, which it does not do by default. - Unicode (
u) changes how the pattern engine interprets the pattern itself, enabling proper handling of Unicode code points beyond the basic multilingual plane, and is required for some newer regex features.
A pattern that works perfectly in a quick single-line test can behave differently once it's run against multiline input, simply because the multiline flag wasn't considered.
Regex Behavior Varies by Language
This is the single most important caveat when testing regex online: regular expression syntax is not fully standardized across languages. JavaScript, Python, PCRE (used by many tools and languages including PHP), Java, and .NET all implement mostly-compatible but not identical regex engines. Differences show up in areas like:
- Named capture group syntax (
(?<name>...)is widely supported now, but support and exact syntax varies by engine version). - Lookbehind assertions (
(?<=...),(?<!...)) are supported in JavaScript, Python, and PCRE, but historically had inconsistent or missing support in some engines. - Unicode property escapes (
\p{...}) require specific flags or engine versions to work. - Possessive quantifiers and atomic groups, common in PCRE-family engines, don't exist in JavaScript's regex engine at all.
The practical implication: testing a pattern in a JavaScript-based tool is a great way to iterate quickly and understand whether your logic is right, but if the pattern will ultimately run in Python, a database query, or another language, always do a final verification in that actual target environment before relying on it in production.
A Practical Testing Workflow
- Write your match/no-match examples first, as plain test strings.
- Build the pattern incrementally, testing after each addition.
- Paste all your test strings together into a test tool and confirm the highlighted matches are exactly the ones you expect, no more, no fewer.
- Check capture groups, if you're using them, to confirm they extract the right sub-strings.
- Re-verify in your target language if it isn't JavaScript, since subtle differences can change behavior.
Try It
Our Regex Tester highlights every match live as you type, shows capture groups and named groups, and lets you toggle flags to see exactly how they change matching behavior, all without sending your pattern or test text anywhere. It's built on JavaScript's native regex engine, so keep the cross-language caveat above in mind for patterns headed to a different runtime.