10 min read1,947 words

Regex Cheat Sheet for Everyday Web Work

Illustration of a printed reference cheat sheet

Regular expressions solve a small number of problems well: validating shape, extracting substrings, and splitting text on patterns rather than fixed characters. This is a working reference for the syntax and patterns that come up most often in day to day web development, along with the traps that catch people out. For a quick lookup while you write a pattern, the regex cheat sheet at /cheat-sheets covers the same ground in a condensed format.

regexreferencejavascript
Share on XHacker News

Character classes

A character class matches one character from a defined set. \d matches a digit, \w matches a word character (letters, digits and underscore), and \s matches whitespace. Their uppercase forms, \D, \W and \S, match the complement. Inside square brackets you define your own set, for example [aeiou] matches any vowel and [^aeiou] matches anything except a vowel.

Ranges inside brackets use a hyphen, so [a-z0-9] matches a lowercase letter or a digit. A hyphen at the start or end of the class, like [-az], is treated as a literal character rather than a range marker, which is a common way to include a hyphen without escaping it.

  • \d, \w and \s cover digits, word characters and whitespace
  • [abc] matches any one of a, b or c
  • [^abc] matches any character that is not a, b or c
  • A hyphen at the edge of a class is literal, not a range

Quantifiers and greediness

Quantifiers control how many times the preceding token can repeat. * means zero or more, + means one or more, ? means zero or one, and {n,m} means between n and m times. By default these are greedy, meaning they consume as much input as possible and then backtrack until the rest of the pattern matches.

Adding a ? after a quantifier makes it lazy instead, so it consumes as little as possible. Given the string <a><b>, the pattern <.+> greedily matches the whole string including both tags, while <.+?> lazily matches just <a>. Lazy quantifiers are the usual fix when a greedy pattern matches more than you expected across multiple delimiters.

  • * zero or more, + one or more, ? zero or one
  • {2,4} between two and four repetitions, {3,} three or more
  • Greedy quantifiers take as much as possible then give back
  • Appending ? to a quantifier makes it lazy

Anchors, groups and named groups

^ anchors to the start of the string or line, and $ anchors to the end, depending on the multiline flag. \b marks a word boundary, useful for matching whole words without matching inside longer words, for example \bcat\b will not match "category".

Parentheses create a capturing group, which both groups a subpattern for quantification and captures the matched text for later use. (?:...) is a non-capturing group, useful when you need grouping without the overhead of a capture. (?<name>...) is a named capturing group, and the match is then available as match.groups.name in JavaScript, which is far more readable than numbered groups once a pattern has more than two or three of them.

  • ^ and $ anchor to string or line boundaries
  • \b matches a word boundary
  • (...) captures, (?:...) groups without capturing
  • (?<name>...) gives you match.groups.name instead of a numeric index

Lookaround

Lookahead and lookbehind assert that something does or does not follow or precede the current position, without consuming characters. (?=...) is positive lookahead, (?!...) is negative lookahead, (?<=...) is positive lookbehind, and (?<!...) is negative lookbehind.

A common use is matching a number only when followed by a specific unit, such as \d+(?=kg), which matches the digits in "5kg" but not the "kg" itself. Lookbehind support varies by engine version, so if you are targeting older browsers or non-JavaScript environments, check compatibility before relying on it.

Flags, including u and v

Flags change how a pattern is interpreted across the whole string. i makes matching case-insensitive, g finds all matches instead of stopping at the first, m makes ^ and $ match at line boundaries within a multiline string, and s makes . match newlines as well.

The u flag switches the engine to treat the pattern and input as full Unicode code points rather than UTF-16 code units, which matters for characters outside the basic multilingual plane, such as many emoji. The newer v flag is a superset of u with improvements to set operations inside character classes, including union, intersection and difference, and stricter syntax that catches more mistakes at parse time. Prefer v over u in new code where your runtime supports it.

  • i case-insensitive, g global, m multiline, s dot matches newline
  • u treats the string as Unicode code points
  • v is the newer superset of u with set operations in character classes

Common recipes

Full email validation according to the specification is impractical with a regular expression, so most real projects use a pragmatic pattern that rejects obviously malformed input and leaves final verification to sending a confirmation email. Something like ^[^\s@]+@[^\s@]+\.[^\s@]+$ is enough to catch missing @ signs or domains without trying to encode the entire email grammar.

A slug pattern such as ^[a-z0-9]+(?:-[a-z0-9]+)*$ validates lowercase words separated by single hyphens, useful for URL segments. An ISO date pattern like ^\d{4}-\d{2}-\d{2}$ checks the shape but not the calendar validity of the date, so pair it with an actual date parser if you need to reject 2026-02-30. Collapsing duplicate whitespace is a one-liner: replace /\s+/g with a single space.

  • Email: check for shape, not full RFC compliance
  • Slug: ^[a-z0-9]+(?:-[a-z0-9]+)*$
  • ISO date shape: ^\d{4}-\d{2}-\d{2}$, validate the calendar separately
  • Collapse whitespace: replace(/\s+/g, " ")

Catastrophic backtracking

Some patterns can take exponential time on certain inputs because the engine tries an enormous number of ways to backtrack before failing. This typically happens with nested quantifiers over overlapping character sets, such as (a+)+b applied to a long string of a characters with no trailing b. The engine explores every way of splitting the a characters between the inner and outer quantifiers before giving up.

The fix is usually to remove the ambiguity: use a non-capturing group with a single quantifier instead of nesting them, be as specific as possible in character classes so alternatives do not overlap, and anchor patterns so the engine can fail fast. If you accept regular expressions from users or process untrusted input with them, this is a real denial of service risk, not just a performance curiosity.

Testing safely

Test patterns against a representative set of inputs, including edge cases like empty strings, strings with only whitespace, and strings that are almost valid but wrong in one place. Testing entirely client-side means you never need to send real user data anywhere to check a pattern, which matters when the sample input includes anything sensitive such as an email address or an account number.

A dev tools hub like /dev-tools is useful alongside a regex tester because you often need to check the raw string you are testing against first, for example decoding a URL-encoded value or checking whitespace and line-ending characters that are invisible in a normal text editor.

Worked example: parsing a log line

Consider a log line like 2026-07-18 14:32:01 ERROR [auth-service] Failed login for user 4821. A pattern such as ^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>\w+) \[(?<service>[\w-]+)\] (?<message>.+)$ pulls out five named groups in one pass: date, time, level, service and message. Building the pattern incrementally, field by field, and testing after each addition is far more reliable than writing the whole thing at once and debugging why it does not match.

Once the pattern matches, match.groups gives you a plain object with those five keys, which is usually more useful downstream than an array of numbered captures, especially once someone else has to maintain the code later and cannot easily tell what group 3 was supposed to mean.

Edge cases that break naive patterns

Empty input often matches patterns built around * rather than +, since zero repetitions is a valid match for a star quantifier. If an empty string should be rejected, use + or an explicit length check rather than assuming a non-empty match implies non-empty input.

Multiline input trips up ^ and $ when the m flag is missing, since without it they anchor to the start and end of the whole string rather than each line. Trailing whitespace, mixed line endings (\r\n versus \n), and BOM characters at the start of a file are all invisible in a normal editor but can cause an otherwise correct pattern to fail on real-world input.

  • A star quantifier can match zero characters; use + if that is wrong
  • Missing the m flag means ^ and $ only match string boundaries, not line boundaries
  • \r\n line endings and BOM characters are invisible but affect matching
  • Unicode strings need the u or v flag when characters outside the basic multilingual plane are involved

Debugging checklist

When a regular expression is not matching what you expect, work through a short list of likely causes before rewriting the whole pattern. First, check the flags: a missing g means only the first match is returned, a missing i means case matters, and a missing m or s changes how ^, $ and . behave.

Second, check for accidental greediness by testing against an input with more than one delimiter, since a pattern that looks right on a simple example can misbehave the moment the real data has two of whatever character it expects only one of. Third, check escaping: characters like ., *, +, ?, (, ) and | have special meaning and need a backslash to be matched literally, and forgetting to escape a literal dot in something like a version number pattern is one of the most common small mistakes in everyday regex work.

  • Confirm the flags being used, especially g, i, m and s
  • Test against input with repeated delimiters to catch greedy over-matching
  • Escape literal special characters: . * + ? ( ) | [ ] { } ^ $ \\
  • Log the actual matched substring, not just whether match() returned truthy

Regex differences across languages

JavaScript, Python, PCRE (used by PHP and many command-line tools) and POSIX regex all share the same broad concepts but differ in details. Python requires (?P<name>...) for named groups rather than JavaScript's (?<name>...), though recent Python also accepts the JavaScript-style syntax in some contexts. POSIX basic regular expressions, used by tools like grep without the -E flag, treat +, ? and | as literal characters unless escaped, which is the opposite of every other flavour mentioned here and a frequent source of confusion when a pattern that works in JavaScript does nothing useful on the command line.

Lookbehind support is another point of divergence: it was added to JavaScript relatively recently compared to PCRE, so code that needs to run in older JavaScript engines sometimes has to restructure a pattern to avoid lookbehind entirely, usually by capturing the preceding context instead of asserting it.

FAQ

Why does my pattern match too much? Almost always a greedy quantifier spanning more of the string than intended; try a lazy quantifier or a more specific character class that excludes the delimiter.

Why does test() keep returning different results for the same string? A pattern with the g flag keeps internal state (lastIndex) across calls on the same regex object, so repeated calls without resetting lastIndex can silently skip or miss matches. Create a fresh regex literal, or reset lastIndex to 0, when reusing a global pattern in a loop.

Can a regex validate that a date is real, not just correctly shaped? No, not without an impractically large pattern accounting for month lengths and leap years. Check the shape with regex and validate the actual calendar date with a date library or the built-in Date object.

Questions about the tools in this guide

Short answers about the hubs this article touches, each linking straight to the tool.

Dev Tools

Open hub

Cheat Sheets

Open hub