RegExp.escape in JavaScript
RegExp.escape in JavaScript solves one narrow but painful problem: turning ordinary text into a safe literal fragment for a dynamic regular expression. It does not validate the text, sanitize HTML, or make regex simple. It prevents user-provided characters like ., +, (, ), [, ], and \ from being interpreted as regex syntax when you meant to search for those characters literally.
That distinction matters in search boxes, text highlighters, route matchers, log filters, and any feature that builds a RegExp from a string.
MDN lists RegExp.escape() as Baseline 2025, newly available across current browser versions since May 2025. If you support older browsers or older runtimes, feature-detect it or use a maintained polyfill.
The problem: strings become regex syntax
The RegExp constructor accepts a string pattern:
const pattern = new RegExp("cat", "gi");
console.log(pattern.test("The catalog has a Cat section.")); // true
That looks harmless until the pattern comes from a user, search field, URL parameter, or database value.
function findMatches(text, searchTerm) {
const pattern = new RegExp(searchTerm, "gi");
return text.match(pattern) ?? [];
}
This works for simple terms:
findMatches("JavaScript and Java", "java");
// ["Java", "Java"]
Then a user searches for ".":
findMatches("v1.2.3", ".");
// ["v", "1", ".", "2", ".", "3"]
The dot did not mean a literal period. In regex, . means "almost any character." The function is no longer searching for user text. It is executing user text as regex syntax.
That is the bug.
Use RegExp.escape for literal text
Use RegExp.escape() before inserting ordinary text into a dynamic regex.
function findMatches(text, searchTerm) {
const escapedTerm = RegExp.escape(searchTerm);
const pattern = new RegExp(escapedTerm, "gi");
return text.match(pattern) ?? [];
}
console.log(findMatches("v1.2.3", "."));
// [".", "."]
Now the user's dot is treated as a dot.
The same fix handles terms that contain regex syntax:
const text = "Use C++ for this example, not C#.";
console.log(findMatches(text, "C++"));
// ["C++"]
Without escaping, "C++" is not a safe literal pattern. The + characters have regex meaning. In some patterns they can even produce syntax errors.
What RegExp.escape actually protects
RegExp.escape() protects the regex boundary. It makes a string safe to use as a literal part of a larger regular expression.
That includes characters such as:
| User text | Why it is risky in regex |
|---|---|
. | Matches almost any character |
* and + | Repeat the previous token |
? | Makes a token optional or changes greediness |
( and ) | Create groups |
[ and ] | Create character classes |
^ and $ | Match positions |
| ` | ` |
\ | Starts an escape sequence |
It also handles less obvious cases. For example, MDN warns against trying to recreate the method with a quick replaceAll() because RegExp.escape() uses escape forms that work in more contexts than a simple backslash-before-punctuation helper.
That boring sentence hides real bugs. Some characters are not safe to escape with a plain backslash in every regex context. Others can accidentally join with a previous escape sequence when inserted into a larger pattern.
A safer highlighter
A search highlighter is the classic example.
The naive version:
function highlight(text, term) {
const pattern = new RegExp(term, "gi");
return text.replace(pattern, (match) => `<mark>${match}</mark>`);
}
It seems fine:
highlight("Learn JavaScript", "java");
// "Learn <mark>Java</mark>Script"
Then the term is ".":
highlight("v1.2.3", ".");
// "<mark>v</mark><mark>1</mark><mark>.</mark><mark>2</mark><mark>.</mark><mark>3</mark>"
That is not highlighting periods. That is wrapping almost every character.
Escape the term:
function highlight(text, term) {
if (term === "") {
return text;
}
const escapedTerm = RegExp.escape(term);
const pattern = new RegExp(escapedTerm, "gi");
return text.replace(pattern, (match) => `<mark>${match}</mark>`);
}
console.log(highlight("v1.2.3", "."));
// "v1<mark>.</mark>2<mark>.</mark>3"
That fixes the regex bug. It does not fix every output-safety problem.
If text or term came from a user and the result will go into innerHTML, you still need to treat HTML separately. Regex escaping and HTML escaping are different jobs. For DOM output, read textContent vs innerHTML vs innerText.
RegExp.escape is not HTML escaping
This is the boundary people blur:
const userText = "<img src=x onerror=alert(1)>";
const safeForRegex = RegExp.escape(userText);
safeForRegex is safer to insert into a regex pattern. It is not safe HTML. It is not safe SQL. It is not safe shell input. It is not proof that the text is allowed.
Escaping always belongs to a target language:
| Target | Escaping job |
|---|---|
| Regex pattern | Make text literal inside a regex |
| HTML text | Display text without parsing markup |
| URL query | Encode query data |
| SQL query | Use parameters, not string building |
| Shell command | Avoid string-built commands |
RegExp.escape() only covers the first row.
Building a larger pattern
The safest pattern is to escape only the literal pieces that come from data, then write your own regex syntax around them.
Suppose you want to match a whole tag name, case-insensitively:
function hasTag(tagsText, tagName) {
const escapedTag = RegExp.escape(tagName);
const pattern = new RegExp(`(^|,)\\s*${escapedTag}\\s*(,|$)`, "i");
return pattern.test(tagsText);
}
console.log(hasTag("js, c++, css", "c++")); // true
console.log(hasTag("js, css", "c+")); // false
The separators and whitespace rules are regex syntax you wrote intentionally:
(^|,) start of string or comma
\\s* optional whitespace
tag escaped literal tag
\\s* optional whitespace
(,|$) comma or end of string
Only tagName is escaped, because only tagName is data.
That is the model to keep:
Author-written regex syntax stays syntax.
User-written text becomes literal text.
Do not use RegExp.escape when the user is writing regex
Sometimes the feature is supposed to accept regex syntax. A developer-facing log search might allow patterns like:
error|failed|timeout
If you pass that through RegExp.escape(), you remove the user's regex power:
const escaped = RegExp.escape("error|failed|timeout");
const pattern = new RegExp(escaped, "i");
pattern.test("request failed");
// false
The | is treated literally, so the pattern searches for the exact text "error|failed|timeout".
That is correct if the input is ordinary text. It is wrong if the input is a real regex pattern.
Pick one contract:
| Feature contract | Use RegExp.escape? |
|---|---|
| User enters plain search text | Yes |
| User enters a username, tag, domain, or filename | Yes |
| User enters a developer regex pattern | No |
| Your code combines fixed regex syntax with user text | Escape the user text only |
Do not quietly support both plain text and regex syntax in the same box unless the UI makes that mode obvious. Ambiguous search boxes create bugs because . sometimes means "dot" and sometimes means "any character."
Feature detection
Because RegExp.escape() is relatively new, check support if your code runs outside current browsers.
if (typeof RegExp.escape !== "function") {
throw new Error("RegExp.escape is not available in this runtime.");
}
For app code, a maintained polyfill is usually better than a tiny custom replacement. The hard part of RegExp.escape() is not the common characters you remember. It is the edge cases you do not remember.
For very simple searches, you may not need regex at all:
function includesText(text, term) {
return text.toLowerCase().includes(term.toLowerCase());
}
Use a regex when you need regex behavior: flags, boundaries, replacement callbacks, repeated matching, or structured surrounding syntax. Use string methods when you only need a string method. Heroism is not required.
Common mistakes
Escaping the whole pattern
This removes your own regex syntax:
const pattern = RegExp.escape(`^${userName}$`);
Escape the data, then build the pattern:
const pattern = `^${RegExp.escape(userName)}$`;
Escaping after constructing the regex
This is too late:
const pattern = new RegExp(userInput, "gi");
const escaped = RegExp.escape(pattern.source);
The constructor has already interpreted the input as regex syntax. Escape before new RegExp(...).
Using regex when includes is enough
This is more machinery than needed:
function hasKeyword(message, keyword) {
return new RegExp(RegExp.escape(keyword), "i").test(message);
}
For plain case-insensitive containment, a string method is clearer:
function hasKeyword(message, keyword) {
return message.toLowerCase().includes(keyword.toLowerCase());
}
Reach for RegExp.escape() when dynamic regex is actually the right tool.
Practice the decision
Use RegExp.escape() when all three are true:
- You are constructing a
RegExp. - Some part of the pattern comes from data.
- That data should be matched literally.
Do not use it as a general sanitizer. Do not use it because "the input seems dangerous." Name the target boundary.
If the boundary is regex, escape for regex. If the boundary is HTML, write text with textContent. If the boundary is a query string, use URL tools. The habit is not memorizing one magic method. The habit is knowing what language your string is about to enter.
Key takeaways
RegExp.escape()turns ordinary text into a safe literal fragment for a dynamic regular expression.- Escape user-provided pieces before passing the final pattern string to
new RegExp(...). - Do not use
RegExp.escape()for HTML, SQL, shell commands, or general validation. - If users are intentionally writing regex syntax, do not escape their whole pattern.
- For simple containment checks, string methods like
includes()can be clearer than dynamic regex.