Tools Nimbus

Why is my regex not matching

Tools Nimbus is a free, no-signup developer toolkit that runs entirely in your browser, so your data is never uploaded to a server. A regex usually fails to match because the global flag advanced its lastIndex, a special character was not escaped, or a flag such as ignore-case was off. Paste your pattern into the Tools Nimbus Regex Tester to see live matches, capture groups, and flag toggles, free with no signup.

Last updated June 2026

Start by isolating the failure

A regex that does not match is almost never random. It is one of a small set of well-known traps: a flag that is on (or off) when it should not be, a metacharacter being read literally or a literal being read as a metacharacter, or invisible characters in the text. The fastest way to debug is to separate the pattern from the surrounding code. Drop the raw pattern and a sample of the real text into the Regex Tester, which uses the same JavaScript engine as your browser and Node.js, and watch which parts highlight. If it matches there but not in your code, the bug is in how your code calls the regex, not in the pattern itself.

Symptom, cause, and fix at a glance

SymptomMost likely causeFix
Matches once, then test() returns falseGlobal flag advancing lastIndex on a reused regexReset lastIndex = 0, or use match() / a fresh regex
A literal dot, plus, or bracket never matchesMetacharacter being read as a special tokenEscape it: \., \+, \[
Misses uppercase or lowercase variantsCase-sensitive by defaultAdd the i flag
Does not span multiple lines. excludes newline; ^$ anchor the whole stringAdd s for dotAll, and/or m for per-line anchors
Works as a literal, breaks via new RegExpBackslashes eaten by the stringDouble them: "\\d+"
Identical-looking text still failsHidden characters: \r\n, non-breaking space, zero-widthNormalize whitespace; inspect in a tester

Cause 1: the global flag and lastIndex (the sneaky one)

This is the bug that makes a regex appear to work intermittently. When a regex has the g flag, the methods test() and exec() are stateful: each call stores where it stopped in a lastIndex property and the next call resumes from there. If you keep one regex object around and call test() in a loop or across inputs, every other call can return false even though the string clearly matches.

const re = /cat/g;
re.test("cat");   // true,  lastIndex now 3
re.test("cat");   // false, resumed from index 3
re.test("cat");   // true again

The fix is to not carry state. Use a literal without g for a simple yes/no check, reset re.lastIndex = 0 before each call, or switch to String.prototype.match() and String.prototype.matchAll(), which do not mutate the regex.

Cause 2: unescaped metacharacters

The characters . + * ? ( ) [ ] { } ^ $ | and the backslash all have meaning in a pattern. A common mistake is writing /3.14/ to match the string 3.14; the dot is a wildcard, so it also matches 3x14 and fails to be the precise check you wanted. To match a literal, escape it: /3\.14/. The reverse trap is just as common: when you build a pattern from user input or a filename, special characters in that input silently change the pattern. Escape the dynamic part first, for example by replacing every metacharacter with its escaped form before constructing the regex.

Cause 3: the wrong flags

Flags are the single most common reason a pattern that looks right does not match. Three matter most:

  • i makes matching case-insensitive. Without it, /error/ never matches Error.
  • g finds every match instead of just the first. If you only get one result, you probably forgot g (but see Cause 1 for its side effects).
  • m and s control multi-line text. m makes ^ and $ match at each line break; s (dotAll) lets . match newline characters. They are independent, and a pattern that needs to cross lines usually needs s.

The Regex Tester exposes g, i, m, s, u, and y as toggles, so you can flip one on and watch the match set change without editing your code.

Cause 4: string-built patterns and double escaping

A pattern that works as a literal can break the moment you move it into a string for new RegExp(). The string parser consumes backslashes before the regex engine ever sees them, so "\d+" becomes the literal text d+. You have to double every backslash:

/\d{3}/                  // literal: matches three digits
new RegExp("\d{3}")     // wrong: matches "ddd"
new RegExp("\\d{3}")   // correct: matches three digits

When in doubt, prefer a literal regex. Reserve new RegExp() for cases where the pattern is genuinely dynamic, and remember the extra backslashes when you do.

Cause 5: invisible characters in the text

Sometimes the pattern is perfect and the text is the problem. Text copied from a PDF, a spreadsheet, or a chat app often contains a non-breaking space (U+00A0) that looks exactly like a normal space but is a different code point, so / / misses it. Windows line endings (\r\n) leave a stray carriage return that $ may not sit where you expect. Zero-width characters and a trailing newline cause the same kind of silent miss. Normalize the input first, for example collapsing whitespace, before matching. Pasting the real text into a tester makes these gaps visible because you can see exactly where the highlight stops.

A note on Unicode

By default a regex works on UTF-16 code units, not whole characters, so . can match half of an emoji or an astral-plane character and produce broken results. Add the u flag for correct Unicode handling. Note too that \w and \d only cover ASCII, so accented letters and non-Latin digits will not match \w unless you use explicit Unicode property escapes such as \p{L} together with the u flag.

Diagnose it in one step

You do not have to guess. Open the Regex Tester, paste your exact pattern and a representative slice of the real text, and toggle flags one at a time. It runs entirely in your browser, so log lines, tokens, or customer data you are testing against never leave your machine. If the text came out of a JSON payload, clean it up first with the JSON Formatter, or decode an encoded value with the URL Encoder and Decoder before matching. For more developer fixes, browse the Tools Nimbus guides.

How to prevent it

  • Decide your flags deliberately. Most bugs come from a missing i, s, or m, or an unwanted g.
  • Never call stateful test() or exec() on a shared global regex inside a loop without resetting lastIndex.
  • Escape any user-supplied text before building a pattern from it.
  • Prefer literal regexes; only reach for new RegExp() when the pattern is dynamic, and double your backslashes when you do.
  • When a match fails on text that looks correct, suspect invisible characters and test in the Regex Tester before changing code.

Frequently asked questions

Why does my regex only match the first time and then fail?+

You are reusing one regex object that has the global flag set. With the g (or y) flag, test() and exec() remember a lastIndex and resume from it on the next call, so the second call starts partway through the string and can return false even when a match exists. Either create a fresh regex each call, reset regex.lastIndex to 0, or use String.match() or String.matchAll() instead, which do not carry state.

Why does my regex not match a dot, plus, or bracket in the text?+

Characters like . + * ? ( ) [ ] { } ^ $ | and the backslash are regex metacharacters, so they mean something special rather than the literal symbol. To match a literal dot, escape it as \. and to match a literal plus use \+. When you build a pattern from user input, escape the whole string first so none of those characters change the meaning of the pattern.

Why does my regex miss uppercase or lowercase versions?+

Regex is case sensitive by default, so /cat/ will not match Cat or CAT. Add the i flag to match regardless of case, for example /cat/i. If you only need case-insensitivity in one part of a larger pattern, a case-insensitive group is usually simpler than splitting the regex.

Why does my regex not match across multiple lines?+

Two different flags control multi-line behavior and they do different things. The m flag makes ^ and $ match at the start and end of each line rather than the whole string. The s (dotAll) flag makes the dot match newline characters too. If your pattern spans several lines, you usually need s, and if you are anchoring per line you need m. They are independent and can be combined.

Why does my regex from new RegExp behave differently than a literal?+

When you build a regex from a string with new RegExp("..."), backslashes are consumed first by the string itself, so \d in a string is just d. You need to double them: new RegExp("\\d+") is the same as /\d+/. Literal regexes written with slashes do not have this problem, which is why a pattern that works as a literal can break when moved into a string.

Why does my regex fail on text that looks identical?+

Invisible characters are the usual cause: a trailing newline, a Windows \r\n line ending, a non-breaking space (U+00A0) instead of a normal space, or a zero-width character pasted from a document. They look the same on screen but are different code points, so an exact pattern misses. Paste both the pattern and the text into a tester so you can see matches highlighted and spot the gap.

Try these browser-based tools mentioned in this guide. Everything runs locally, so your data never leaves your device.