Regular Expressions in JavaScript: A Field Guide for Working Developers
The regex syntax you actually use, how JavaScript flags change matching, why greedy quantifiers surprise people, and how to avoid catastrophic backtracking.
Regular expressions have a reputation as write-only code. That reputation is earned by patterns written in one go and never tested. Written a piece at a time and checked against real input, regex is one of the most productive tools a developer has.
This is a practical guide to the parts of JavaScript regex you will actually use, the flags that change behaviour, and the traps that catch experienced people. Keep the Regex Tester open and try each example.
A little history
Regular expressions come from mathematics. In the 1950s Stephen Kleene described "regular sets" and the notation for them. Ken Thompson turned the idea into software in the late 1960s, building regex search into the QED editor and then ed, which gave us grep (from the ed command g/re/p: globally search for a regular expression and print). Perl, released in 1987, added much of what we now consider standard syntax, and most modern flavours, including JavaScript's, descend from Perl's.
That shared ancestry is why regex looks similar everywhere, and the differences between flavours are why a pattern copied from a Python answer sometimes fails in Node.js.
The core syntax
| Token | Meaning | Example |
|---|---|---|
. | Any character except a newline | a.c matches abc, a-c |
\d \w \s | Digit, word character, whitespace | \d{4} matches 2026 |
\D \W \S | The opposite of each | \S+ matches a run of non-space |
[abc] [a-z] | Character class | [aeiou] matches a vowel |
[^abc] | Negated class | [^,]+ matches up to a comma |
^ $ | Start and end of input (or line with m) | ^\d+$ is digits only |
\b | Word boundary | \bcat\b skips concatenate |
* + ? | 0 or more, 1 or more, 0 or 1 | colou?r |
{n,m} | Between n and m times | \d{2,4} |
( ) | Capturing group | (\d+)-(\d+) |
(?: ) | Non-capturing group | `(?:http |
(?<name> ) | Named group | (?<year>\d{4}) |
| `a | b` | Alternation |
Note that \w in JavaScript means [A-Za-z0-9_]. It does not include accented letters unless you use Unicode property escapes like \p{L} with the u flag.
Flags change everything
In JavaScript the flags sit outside the pattern: /pattern/gi. In the tester they go in the Flags box.
| Flag | Effect |
|---|---|
g | Global: find all matches, not just the first |
i | Case-insensitive |
m | Multiline: ^ and $ match at line breaks |
s | dotAll: . also matches newlines |
u | Unicode: correct handling of code points and \p{...} |
y | Sticky: match only at lastIndex |
Forgetting g is the most common reason someone thinks a pattern "only finds one match". Forgetting m is why ^ERROR finds nothing in a multi-line log: without it, ^ means the start of the whole input.
Greedy, lazy and the .* trap
Quantifiers are greedy by default: they match as much as they can, then give back characters only if the rest of the pattern fails. Try <.+> against <b>bold</b>. You might expect <b>, but you get the whole string, because .+ runs to the end and backs off only as far as the last >.
Two fixes:
- Lazy quantifier:
<.+?>matches as little as possible, giving<b>and</b>. - Negated class:
<[^>]+>says what you mean, "anything except>", and is usually faster.
I prefer the negated class. It makes the pattern's intent explicit and avoids a lot of backtracking.
Worked examples
Here are patterns I have actually shipped, with the reasoning.
An ISO date (YYYY-MM-DD):
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
This checks the shape and rules out month 13 or day 32. It does not know February has 28 or 29 days. Use regex for format, and a date library for calendar validity.
Pulling request IDs out of logs:
req_id=(?<id>[a-f0-9-]{36})
The named group makes the extraction code read clearly: match.groups.id.
A semantic version:
^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?$
Major, minor, patch, and an optional pre-release tag such as -beta.2.
Email addresses. Don't. The full grammar in RFC 5322 is famously hard to express in a regex. For a form, something permissive like ^[^\s@]+@[^\s@]+\.[^\s@]+$ catches typos, and sending a confirmation email is the only real test.
Catastrophic backtracking
Some patterns take exponential time on certain inputs. The classic shape is a nested quantifier over overlapping alternatives, such as (a+)+$ or (\w+\s?)*$. On an input like aaaaaaaaaaaaaaaaaaaaaaaa! the engine tries every way of splitting the as between the inner and outer groups before giving up.
This is not theoretical. In July 2019, a regex in a Cloudflare WAF rule caused CPU exhaustion across its network and a global outage, as described in Cloudflare's public post-mortem. When a pattern is applied to user input, this class of bug becomes a denial-of-service vulnerability, known as ReDoS.
To avoid it:
- Avoid nested quantifiers on the same characters.
- Prefer specific classes (
[^,]*) over.*. - Anchor patterns where you can.
- Limit input length before matching.
- Test with long, almost-matching input, not just happy-path samples.
A testing habit that saves time
Whenever I write a non-trivial pattern, I paste three kinds of text into the tester: lines that must match, lines that must not match, and edge cases (empty strings, very long lines, Unicode). Most regex bugs are patterns that are too permissive, and you only see that by testing negative cases.
Then I copy the pattern into code with a comment showing one matching and one non-matching example. Future readers, including me, will thank me.
JavaScript-specific notes
- Lookbehind
(?<=...)and(?<!...)arrived in ES2018 and work in all modern browsers. String.prototype.matchAllreturns every match with its groups, and requires thegflag.- A
RegExpobject withgoryis stateful: it rememberslastIndexbetween calls totest()andexec(). Reusing one regex object across inputs can give alternating true and false results. Create a new one, or resetlastIndex = 0. - The
vflag (ES2024) adds set operations in character classes for advanced Unicode work.
Related tools
- Online Diff Tool to check the effect of a regex search-and-replace
- Case Converter for the renaming jobs people often reach for regex to do
- JSON Formatter, where JSONPath is often a better choice than regex for structured data