A regex pattern is built from characters that mean two completely different things depending on context: most are literal, but thirteen of them are syntax — ^ $ \ . * + ? ( ) [ ] { } |, plus the / delimiter some engines use. Drop a string containing any of these into a pattern without escaping first, and you're not matching that string — you're running whatever regex it accidentally spells out.
Why this bites in practice
The classic trigger is building a pattern from user input at runtime — a "highlight this exact search term" feature, for instance:
const term = "3.99"; // user typed this, expecting a literal price
const re = new RegExp(term);
re.test("3x99"); // true — unescaped, "." matches ANY character, not just a literal dotThe user searched for the literal string 3.99 and got matches for 3x99, 3 99, and anything else with one character between 3 and 99 — not a bug in the regex engine, just . doing exactly what it's specified to do: match any character. The fix is escaping the term before it becomes part of the pattern, so every character in it is guaranteed to mean only itself:
const term = "3.99";
const re = new RegExp(escapeRegex(term)); // "3\.99"
re.test("3x99"); // false — correct
re.test("3.99"); // true — correctThe classic approach: escape only the metacharacters
The straightforward, widely-implemented approach is a single regex replace that backslash-prefixes exactly the characters with syntactic meaning and leaves everything else untouched:
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
}This matches what Python's re.escape(), PHP's preg_quote(), and most other languages' built-in equivalents produce — compact, readable output where only the characters that actually needed escaping got touched.
The native approach: RegExp.escape() (ES2026)
JavaScript shipped a built-in RegExp.escape() in Chrome, Firefox, and Safari starting in early 2025, standardized as part of ES2026 — and it's deliberately more aggressive than the classic approach above. Two extra rules exist specifically to prevent what the proposal calls a "context escape" — the escaped output accidentally combining with whatever text surrounds it into something that means more than the literal string did, even when no individual character in the input was a regex metacharacter:
- Every punctuator gets escaped, not only the thirteen regex metacharacters — comma, hyphen, equals sign, angle brackets, and more all become a
\xHHescape. - The very first character of the string gets a
\xescape if it's a digit or ASCII letter, even though letters and digits are never regex syntax on their own.
The result is uglier but more defensively escaped: RegExp.escape("foo.bar") returns "\x66oo\.bar" — verified directly against MDN's documented examples — not just "foo\.bar" the way the classic approach would produce.
Which one should you use?
- The classic approach for readable output, compatibility with every JavaScript runtime regardless of age, and matching the convention every other language uses.
- Native
RegExp.escape()when you specifically want the extra defensive margin it provides, you're only targeting environments that already have it, or you need output that matches the native method's exact behavior (for testing a polyfill, for instance).
Both produce a result that, wrapped in a real RegExp, matches the original string literally — that correctness property is what actually matters; the aesthetic difference in output is a secondary concern.
Try it yourself
Regex Escape computes both — the classic metacharacter- only form and an exact reproduction of native RegExp.escape() — and verifies the result by actually compiling it into a RegExp and checking it matches your input. Once you have an escaped fragment, Regex Tester lets you drop it into a full pattern and test it against real text. Both run entirely in your browser.