· 6 min read
RegExp.escape: build safe dynamic regex in JavaScript
RegExp.escape turns any string into a literal regex fragment. How it works, where the old escape helper breaks, and where you can use it today.

RegExp.escape() is a built-in JavaScript method that takes a string and returns a version that is safe to put inside a regular expression, so every character matches literally. It replaces the hand-written replace(/[.*+?^${}()|[\]\\]/g, "\\$&") helper that most codebases carry. It has been Baseline since May 2025, so current Chrome, Edge, Firefox and Safari all support it, and so does Node.js 24.
If you ever build a RegExp from user input, a search term or a list of keywords, this is the function you want.
What problem does RegExp.escape solve?
A regular expression is a pattern language where many punctuation characters have special meaning: . matches any character, + means "one or more", parentheses create groups, and so on. When you build a pattern from a string you don't control, those characters stop being text and start being syntax.
const query = "c++ (beta)";
new RegExp(query);
// SyntaxError: Invalid regular expression: /c++ (beta)/: Nothing to repeat
A crash is the good outcome. The bad outcome is a pattern that compiles and quietly matches the wrong thing. A search for node.js also matches nodexjs, because the dot is a wildcard. A highlight feature built this way can be broken by a single bracket in the input.
The usual fix has been a one-line helper copied from somewhere. It handles the common cases, but it has gaps that only show up in unusual inputs, which is exactly why a standard version was added to the language.
How does RegExp.escape work?
You pass a string and get back a string. Then you hand that to the RegExp constructor or splice it into a larger pattern.
const query = "c++ (beta)";
const re = new RegExp(RegExp.escape(query), "gi");
re.source; // "\\x63\\+\\+\\x20\\(beta\\)"
"C++ (Beta) notes".match(re); // ["C++ (Beta)"]
The output looks busier than a hand-escaped pattern, and that is on purpose. The method follows a few rules:
- Syntax characters like
^ $ \ . * + ? ( ) [ ] { } |and/get a backslash in front. - Other punctuation such as
-,,,=,#,&,<,>and quotes become\xhex escapes. A plain backslash would not work here, because something like\-is a syntax error with theuflag outside a character class. - Whitespace is escaped too. A space becomes
\x20, a tab becomes\t, and non-ASCII line breaks use\uescapes. - A leading letter or digit is written as a hex escape. The
fin"foo"becomes\x66.
That last rule looks odd until you see what it prevents. Escaped strings are usually concatenated with other pattern text. If the text before ends in a backreference such as \1, and your escaped string starts with a digit, the two can merge into a different token.
const naive = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
new RegExp("(x)\\1" + naive("1")).test("xx1"); // false
new RegExp("(x)\\1" + RegExp.escape("1")).test("xx1"); // true
With the naive helper, the pattern becomes (x)\11, which no longer means "group one, then a literal 1". RegExp.escape writes the digit as \x31, so it can't fuse with what comes before it.
Passing anything that is not a string throws a TypeError. Convert numbers and other values with String() first.
Where does the hand-rolled helper break?
Most of the time the common helper is fine. Here are the cases where it isn't:
| Input situation | Naive helper | RegExp.escape |
|---|---|---|
c++, node.js, (beta) | Works | Works |
Inside a character class: [ + "a-z" + ] | - is not escaped, so it becomes a range | - becomes \x2d, so it stays literal |
After a backreference like \1, input starting with a digit | Merges into \11 | Leading digit is hex-escaped |
The character class case is easy to hit when you build an "allowed characters" pattern from config:
const chars = "a-z";
new RegExp(`[${naive(chars)}]`).test("m"); // true, it's a range
new RegExp(`[${RegExp.escape(chars)}]`, "v").test("m"); // false
new RegExp(`[${RegExp.escape(chars)}]`, "v").test("-"); // true
None of these are exotic once input comes from users or from data you didn't write.
When should you use RegExp.escape?
Use it whenever a piece of a pattern comes from a variable. A few common shapes:
// Match any of a list of literal keywords
const terms = ["node.js", "c#", "f(x)"];
const re = new RegExp(`(?:${terms.map(RegExp.escape).join("|")})`, "gi");
"I like Node.js and nodexjs".match(re); // ["Node.js"]
// Strip a known domain from URLs
const domain = "example.com";
const host = new RegExp(`^https?://${RegExp.escape(domain)}(?=/|$)`);
RegExp.escape takes a single argument, so passing it straight to map is safe; the extra index and array arguments are ignored.
You don't need it when you only want to find or replace a fixed substring. str.includes(), str.indexOf() and str.replaceAll("literal", "x") with a string argument already treat the text literally, and they are simpler. Reach for a regex, and therefore for escaping, when you need flags like i, anchors, word boundaries or alternation around the literal part.
It also does not make an untrusted pattern safe. If users are allowed to type actual regex syntax, escaping defeats the purpose, and you have a separate problem: patterns that take a very long time to run. Escaping is for literal text only.
Can you use it today?
| Environment | Support |
|---|---|
| Chrome and Edge | 136 and later |
| Firefox | 134 and later |
| Safari | 18.2 and later |
| Node.js | 24 and later; not in Node.js 22 |
Node.js 22 is still a common target, and there RegExp.escape is undefined. If your code has to run on older runtimes, feature-detect and keep your old helper as the fallback:
const escapeRegExp =
typeof RegExp.escape === "function"
? RegExp.escape
: (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
The fallback still has the gaps above, so if you need identical behavior everywhere, use a polyfill; core-js ships one. In TypeScript, the type comes from a recent lib setting. If the compiler says escape doesn't exist on RegExpConstructor, update TypeScript and target a newer lib, or add a short declaration until your setup catches up. This is the same pattern as the native Uint8Array base64 methods, another small built-in that replaces a helper almost every project used to copy around.
Key takeaways
RegExp.escape(str)turns any string into a pattern fragment that matches that text literally.- It escapes more than the usual helper: punctuation like
-, whitespace, and a leading letter or digit, so fragments can't merge with surrounding syntax. - Use it whenever a variable becomes part of a
RegExp; use plain string methods when you don't need regex features. - It is Baseline since May 2025 and available in Node.js 24, but not in Node.js 22.
- It escapes literal text. It does not make user-written regex patterns safe.
FAQ
Why does RegExp.escape turn "foo" into "\x66oo"?
The first character is hex-escaped when it is a letter or digit so it can't combine with whatever comes before it, such as \1 or \x0. The pattern still matches the plain text foo.
Does RegExp.escape work inside character classes?
Yes. Because - and other punctuation are hex-escaped, the result stays literal inside [...], including with the u and v flags.
Should I replace my existing escape helper?
If you target environments that all support it, yes, since the built-in covers edge cases the common one-liner misses. Otherwise feature-detect and fall back, or use a polyfill.