Invalid Regular Expression: Every JavaScript Syntax Error and How to Fix It

Published 6 Sep 2026 · 9 min read

new RegExp(pattern) and regex literals both throw a SyntaxError the moment a pattern can't be parsed at all — before it ever gets the chance to match or fail to match anything. The message always starts the same way, Invalid regular expression: /.../: , followed by a short, specific reason. That reason is usually the fastest route to the fix, but it's also easy to misread if you've never seen it before. This covers seven real causes, the exact V8 message each one produces, and a live, prefilled Regex Lab example for each so you can watch it fail on the exact pattern instead of a description of one.

Quick answer: almost every one of these comes from treating a string that contains regex metacharacters — +, ., *, (, ), [, ], {, }, \, |, ^, $ — as if it were already a valid pattern, most commonly by passing raw user input or a copy-pasted snippet straight into new RegExp() without escaping it first.

Quick reference

All seven results below were verified live in Regex Lab against a real build — each pattern constructed with Node's new RegExp() first to confirm the exact message, then reproduced in the browser via the same V8 engine Chromium ships.

MessageMost likely causeFix
Nothing to repeatA quantifier (+ * ? {n}) with nothing before it to repeatEscape the leading/duplicate metacharacter, or remove it
Unterminated groupAn opening ( with no matching )Close the group, or escape the paren if you meant it literally
Unmatched ')'A closing ) with no opening (Escape it as \) if you meant a literal parenthesis
Unterminated character classAn opening [ with no matching ]Close the class, or escape a literal [
Invalid groupA (?...) sequence JS doesn't recognize (often ported from Python)Use JS syntax — (?<name>...), not (?P<name>...)
numbers out of order in {} quantifierA {min,max} quantifier where min > maxSwap the two numbers
\ at end of patternA trailing, unescaped backslash (often a pasted Windows path)Escape it as \\\\, or strip the trailing slash

1. Nothing to repeat

Quantifiers (+, *, ?, {n,m}) apply to whatever came immediately before them in the pattern. If that's the very first character, or another quantifier, there's nothing for it to repeat. This is the single most common way raw user input breaks a dynamically-built regex, because + is a completely ordinary character in plain text (a phone number's country-code prefix) and an extremely meaningful one in a pattern.

const phoneSearch = '+15555550123'; // pasted from a CSV export
new RegExp(phoneSearch);
// SyntaxError: Invalid regular expression: /+15555550123/: Nothing to repeat

The same error also shows up from a doubled quantifier — typically a copy-paste artifact, or someone trying to "make a wildcard more greedy" by repeating it, which isn't valid syntax:

const pattern = 'sales_report' + '*' + '*'; // meant "match anything", doubled by mistake
new RegExp(pattern);
// SyntaxError: Invalid regular expression: /sales_report**/: Nothing to repeat

Fix either case by escaping the string before it becomes part of a pattern, using the standard one-line escape helper (there's no built-in for this): str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'). For the doubled-quantifier case specifically, a single * already means "zero or more" — delete the extra one.

See it fail live

A customer phone number, used directly as a search pattern instead of as literal text to find:

Open this example in Regex Lab

See it fail live

A "match anything" wildcard, accidentally doubled:

Open this example in Regex Lab

2. Unterminated group

Every ( needs a matching ) somewhere later in the pattern. This shows up constantly when someone wants to search log lines for literal text that happens to contain a parenthesis — typing the text as-is, rather than as an escaped literal, leaves the group open with nothing to close it.

const logFilter = '(unauthorized'; // meant to match the literal text "(unauthorized)"
new RegExp(logFilter, 'i');
// SyntaxError: Invalid regular expression: /(unauthorized/i: Unterminated group

If you want to match a literal parenthesis rather than open a capture group, escape it: \(unauthorized\). If you actually meant to open a group, add the closing ).

See it fail live

Searching log output for a bracketed status tag, missing its closing paren:

Open this example in Regex Lab

3. Unmatched ')'

The mirror image of the previous error: a closing ) that was never opened. It's just as often the exact same log-search scenario, with the pattern trimmed from the other end.

const logFilter = 'unauthorized)'; // the opening paren got trimmed off
new RegExp(logFilter, 'i');
// SyntaxError: Invalid regular expression: /unauthorized)/i: Unmatched ')'

Same fix as above: escape it as \) for a literal parenthesis, or add the matching ( if a group was actually intended.

See it fail live

The same log search, this time missing its opening paren:

Open this example in Regex Lab

4. Unterminated character class

A character class opened with [ needs a closing ]. A common way this slips through: building an "allowed characters" pattern for a username or slug validator by hand, and losing the closing bracket during an edit.

const usernamePattern = '[a-z'; // closing bracket dropped during a refactor
new RegExp(usernamePattern);
// SyntaxError: Invalid regular expression: /[a-z/: Unterminated character class

Add the missing ]: [a-z]. If you meant a literal square bracket rather than a class, escape it instead: \[a-z.

See it fail live

A lowercase-letter character class, missing its closing bracket:

Open this example in Regex Lab

5. Invalid group

Any (? sequence has to continue with a syntax JavaScript actually recognizes: (?: for a non-capturing group, (?= / (?! for lookaheads, (?<= / (?<! for lookbehinds, or (?<name> for a named capture group. The most common way this breaks is porting a regex from Python or PCRE, which spell a named group (?P<name>...) — the extra P isn't valid JavaScript syntax at all.

const isoDate = '(?P<year>\\d{4})-(?P<month>\\d{2})-(?P<day>\\d{2})'; // Python syntax
new RegExp(isoDate);
// SyntaxError: Invalid regular expression: /(?P<year>\d{4})-.../: Invalid group

Drop the P — JavaScript's named groups (supported since 2018, no library needed) are written (?<year>\d{4}).

See it fail live

An ISO date pattern with Python-style named groups:

Open this example in Regex Lab

6. numbers out of order in {} quantifier

A {min,max} quantifier requires min ≤ max. This one tends to appear when a pattern is generated from two form fields (a minimum and maximum length, say) and the values end up swapped — the resulting pattern is not just wrong, it's unparseable.

const minLen = 8, maxLen = 4; // swapped by mistake
const pattern = `.{${minLen},${maxLen}}`;
new RegExp(pattern);
// SyntaxError: Invalid regular expression: /.{8,4}/: numbers out of order in {} quantifier

Fix by validating that min ≤ max before building the pattern string, not after the RegExp constructor has already thrown.

See it fail live

A length-range quantifier built from two swapped variables:

Open this example in Regex Lab

7. \ at end of pattern

A backslash escapes whatever character comes right after it, so a pattern can never end on an unescaped, unpaired \. This is almost always a pasted Windows file path used directly as a matcher — the trailing backslash before the closing quote has nothing left to escape.

const pathFilter = 'C:\\Users\\dev\\'; // trailing backslash, pasted from Explorer's address bar
new RegExp(pathFilter);
// SyntaxError: Invalid regular expression: /C:\Users\dev\/: \ at end of pattern

Escape every backslash in the literal path before it becomes a pattern (C:\\\\Users\\\\dev\\\\, i.e. each single backslash doubled), or run it through the same generic escape helper from the first section — it handles \ along with every other metacharacter.

See it fail live

A Windows path used as a matcher, with its trailing backslash left unescaped:

Open this example in Regex Lab
One more, less common: reusing the same name across two named groups — (?<year>\d{4})-(?<month>\d{2})-(?<year>\d{2}) — throws Duplicate capture group name. It's an easy one to introduce by copy-pasting a group definition twice and forgetting to rename the second copy. See it fail live — rename one of the two year groups to fix it.

Debugging checklist

  1. If the pattern comes from a variable rather than being typed by hand, ask whether that variable could ever contain + . * ( ) [ ] { } \ | ^ $ as ordinary data. If yes, escape it before it reaches new RegExp() — don't wait for the exception.
  2. Read the exact reason after the colon in the error message; it names the specific rule that was broken, not just "your regex is wrong."
  3. Count parens and brackets by hand for "Unterminated"/"Unmatched" errors — they're almost always one character short or one character extra, not a deeper logic problem.
  4. If a pattern was copied from Python, PHP, or a man page, check named groups and comments specifically — (?P<name> and (?#comment) are both invalid JavaScript syntax even though they're valid elsewhere.
  5. For {min,max} errors, print the two numbers right before they're interpolated into the pattern string — a silent swap upstream is the usual cause.

🔍 Paste your pattern and see exactly where it fails

Regex Lab compiles and tests your pattern entirely in your browser, live as you type, and now shows the exact syntax error message instead of just reporting zero matches. Nothing you paste is ever uploaded.

Open Regex Lab →

← Back to Blog