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

TokenMeaningExample
.Any character except a newlinea.c matches abc, a-c
\d \w \sDigit, word character, whitespace\d{4} matches 2026
\D \W \SThe 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
\bWord boundary\bcat\b skips concatenate
* + ?0 or more, 1 or more, 0 or 1colou?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})
`ab`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.

FlagEffect
gGlobal: find all matches, not just the first
iCase-insensitive
mMultiline: ^ and $ match at line breaks
sdotAll: . also matches newlines
uUnicode: correct handling of code points and \p{...}
ySticky: 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.matchAll returns every match with its groups, and requires the g flag.
  • A RegExp object with g or y is stateful: it remembers lastIndex between calls to test() and exec(). Reusing one regex object across inputs can give alternating true and false results. Create a new one, or reset lastIndex = 0.
  • The v flag (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

Launch Regex Tester