Regular expressions in JavaScript: a working guide

The parts of JavaScript regex that cause most wrong matches, each with an input and the exact result. Paste any example into the live Regex Tester to watch it run.

Updated · By Alexander Braz

Which regex flavor this guide covers

Every example here uses the regular expression engine defined by the ECMAScript specification, the one inside browsers and Node.js. Fieldkit's Regex Tester compiles your pattern with new RegExp(pattern, flags), so what you see in the tester is what your JavaScript code will do. Patterns copied from Python, PHP (PCRE), or grep usually work, but not always. The last section lists the differences that bite most often.

One habit makes everything else easier: always test a pattern against text that should not match, not only against the happy case. Most regex bugs are false positives.

Character classes are narrower than they look

The shorthand classes are defined in ASCII terms. \d is exactly [0-9]. \w is [A-Za-z0-9_]. \b, the word boundary, is built on \w, so it inherits the same limits. That surprises people working with names or any non-English text.

Pattern \w+ against café São returns three matches: caf, S, and o. The accented letters are not word characters, so they split the words. Turn on the u flag and use a Unicode property escape instead: pattern \p{L}+ with flag u returns café and São. Property escapes only work in Unicode mode; without u, the engine reads \p as a plain letter p.

The same goes for digits. \d+ against Total: ٣٤ and 34 matches only 34, even with u. The Arabic-Indic digits are not in [0-9]. If you accept user input from many locales, decide on purpose whether those digits should count.

Flags, and what the tester does with them

  • g (global) finds every match instead of stopping at the first. The tester always adds it so it can highlight everything. Your code may not, which matters for String.prototype.match and replace.
  • i ignores case. hello with i matches Hello and HELLO.
  • m (multiline) makes ^ and $ match at the start and end of every line, not only of the whole string.
  • s (dotAll) lets . match line breaks. Without it, . stops at \n.
  • u turns on Unicode mode: property escapes, code point aware matching, and stricter syntax checking.
  • y (sticky) only matches at the exact position in lastIndex. It is mostly useful for tokenizers.

The spec also defines d, which records start and end indices for each group, and v, a stricter Unicode mode with set operations inside classes. The tester ignores both, so test those in your own code.

Worked example: pulling order IDs out of a log

Here is a three-line log. Paste it into the test string box:

2026-10-08 09:14:02 INFO  checkout ok order=48213 total=19.90
2026-10-08 09:14:09 WARN  payment retry order=48214 total=7.50
2026-10-08 09:15:30 INFO  checkout ok order=48215 total=120.00

Only the numbers. Pattern (?<=order=)\d+. The lookbehind (?<=order=) requires the text order= right before the digits but does not include it in the match. Result: 3 matches, 48213, 48214, 48215, and Groups reads 0 because lookarounds do not capture. Copy matches gives you the three IDs, one per line, ready for a SQL IN list.

Only the warning line. Pattern ^.*WARN.*$ with no flags returns no matches at all. Without m, ^ means the start of the whole text and $ the end, and . cannot cross the line breaks in between. Add m and you get exactly one match, the full second line.

Two values per line. Pattern order=(?<id>\d+) total=(?<total>\d+\.\d{2}) returns 3 matches with Groups 2. In code, match.groups.id and match.groups.total read better than match[1] and match[2], and they survive someone adding another group later.

Greedy and lazy quantifiers

*, +, ? and {n,m} are greedy: they take as much as they can, then give back only what is needed for the rest of the pattern to match. Against title="Fieldkit" lang="en", the pattern ".+" returns a single match, "Fieldkit" lang="en", from the first quote to the last one.

Adding ? makes a quantifier lazy. ".+?" returns two matches, "Fieldkit" and "en". A negated class says the same thing more directly and is often faster: "[^"]*" also returns "Fieldkit" and "en", because it simply cannot run past a quote.

Anchors turn a search into a check

A pattern without anchors matches anywhere in the string. If you want to validate a five-digit code, \d{5} is wrong: against order 482139 it happily matches 48213. Use ^\d{5}$. It matches 48213 and rejects 482139. In the tester, remember that m changes what the anchors mean; for single-value validation, leave m off.

Escaping: the dot and the backslash

An unescaped . matches any character except a line break. Pattern 1.5 against 1.5 and 125 finds both 1.5 and 125. Escape it as 1\.5 and only 1.5 matches. The characters that need escaping outside a class are . ^ $ \ | ? * + ( ) [ ] { }, and / as well when you write a regex literal in code.

The tester takes the pattern as you would write it between the slashes of a literal. In JavaScript source, /\d+/ and new RegExp("\\d+") are the same pattern. The string version needs the backslash doubled because the string literal consumes one. Forgetting that is the most common reason a pattern works in the tester and fails in code.

Patterns that can freeze a tab

JavaScript uses a backtracking engine. When a pattern can match the same text in many different ways and the overall match then fails, the engine may try every one of those ways. The classic case is a quantified group that contains another quantifier, such as ^(a+)+$ tested against a long run of a followed by !. Every way of splitting the run between the inner and outer + is a separate path, and each extra character roughly doubles the number of paths.

The tester re-runs on every keystroke, so a pattern like this can make the page unresponsive while you type. The fix is to remove the ambiguity: ^a+$ describes the same strings and fails immediately. Look for nested quantifiers, and for alternations whose branches can match the same text, like (\w|\d)+. In server code this is a security issue, usually called ReDoS, because an attacker can choose the input.

Differences from other flavors

  • No possessive quantifiers or atomic groups. a++ and (?>a) are syntax errors in JavaScript.
  • No \A or \Z. Use ^ and $ without the m flag.
  • Inline flags. PCRE and Python accept (?i) at the start of a pattern. JavaScript does not; put the flag in the flags field instead. Newer editions of the spec add scoped modifiers like (?i:abc), but older engines reject them, so check your runtime before relying on them.
  • Lookbehind is flexible. JavaScript allows variable-length lookbehind, such as (?<=\$\d*), which some other engines refuse.

A short checklist before you ship a pattern

  1. Test it against three inputs that should match and three that should not.
  2. Add anchors if the pattern is a validator.
  3. Decide whether non-ASCII letters and digits should count, and use u with \p{...} if they should.
  4. Scan for nested quantifiers before the pattern sees user input.
  5. Copy it into code with the right escaping, and keep one of your test strings as a unit test.

Open the live tool

Open Regex Tester Related reading: all guides · FAQ.