Regular expressions are powerful, but writing them by rough intuition — "this should match" — breaks things quietly. A pattern that passes your local test misses real input in production, or suddenly stops matching on full-width or multibyte text. The usual cause is a mistaken assumption about the default behavior of a metacharacter or flag. Below are the traps developers hit most often, each laid out as input / intended pattern / actual result / fix.
Input / intended pattern / actual result / fix
Every row below was verified against JavaScript (ECMAScript) regular expressions. The columns are: what you want (goal) / the pattern you wrote / the input you gave / what actually happened / the fix.
| Goal | Pattern | Input | Result | Fix |
|---|---|---|---|---|
Grab only <a> | <.+> | <a><b> | Matches the whole <a><b> (greedy) | Lazy <.+?> → <a> |
| Any single char across a newline | /a.b/ | a\nb (\n = newline) | No match | s (dotAll) → /a.b/s matches |
| Match a digit | \d | Full-width 123 | No match (ASCII digits only) | Normalize to ASCII first, then \d |
| Test for "letters only" | ^\w+$ | 日本語 | No match (\w = [A-Za-z0-9_]) | /^\p{L}+$/u matches |
| Test start/end of each line | /^\w$/g | a\nb | No match (anchors mean whole-string start/end) | Add m → /^\w$/gm → ["a","b"] |
Match a literal . | [.] | . (and a) | Matches ., not a | Inside a class most metacharacters lose their meaning, so the dot is safely literal |
Match exactly cat or dog | /^cat|dog$/ | "catalog" / "hotdog" / "dogma" | true / true / false (parsed as (^cat)|(dog$)) | Group the alternation: /^(cat|dog)$/ ("hotdog" → false) |
| Reuse one regex for repeated tests | const re=/x/g; re.test("x") | "x" twice | true first, false second (lastIndex advances) | Build a fresh literal each time, or drop g (.test() doesn't need it) |
| Test for a single character (emoji) | /^.$/ | "😀" | false (😀 is two code units) | Add the u flag: /^.$/u → true |
Replace every a | "a-a-a".replace(/a/,"b") | "a-a-a" | "b-a-a" (first match only) | Add g: /a/g → "b-b-b" (or replaceAll) |
The ones that bite hardest
The top row — greedy matching — is the most common accident. + and * consume as much as possible by default, so even when you want the match to stop at the first >, it runs to the end of the line. Add ? to make it lazy (shortest match) and you get just <a>.
Full-width digits and Japanese are the other classics. \d and \w look at ASCII only by default, so full-width 123 and 日本語 slip right through. When character type matters, normalize the input to ASCII, or use the u flag with Unicode properties such as \p{L}. The ^/$ anchors are misread just as often: by default they mean the start/end of the whole string. To test each line of a multi-line input you need the m flag — "a\nb".match(/^\w$/gm) returns ["a","b"].
Try it in your browser
These are faster to feel than to reason about. Paste a pattern and input into the Regex tester and you can see the match range, captures, and how each flag behaves on the spot (input is processed entirely in your browser and never sent anywhere). To rewrite every match at once, the Text replace tool is handy.
FAQ
Why doesn't . match a newline?
s (dotAll) flag and . becomes any character including a newline. /a.b/ does not match a\nb, but /a.b/s does.Why doesn't \w match Japanese?
\w is exactly [A-Za-z0-9_] — only ASCII letters, digits, and underscore. To test for "letters" including Japanese, use the u flag with a Unicode property, e.g. /^\p{L}+$/u.Behavior was verified with JavaScript (ECMAScript) regular expressions; other languages and engines differ (as of 2026-07-16).