# Regex cheatsheet: the patterns you actually need

> Quantifiers are greedy by default; add ? to make them lazy.  is the word boundary most people underuse. In JavaScript, a global regex reused across .test() calls is stateful via lastIndex. Nested quantifiers over the same characters cause catastrophic backtracking and hang the page.

Source: https://rankcert.com/blog/regex-cheatsheet
Published: 2026-08-29 · Updated: 2026-09-02

---


Most regex use is a dozen constructs recombined. Here they are, plus the traps specific to JavaScript's engine.

## Character classes

| Pattern | Matches |
|---|---|
| `.` | Any character except newline (unless `s` flag) |
| `\d` `\D` | Digit / not a digit |
| `\w` `\W` | Word character `[A-Za-z0-9_]` / not |
| `\s` `\S` | Whitespace / not |
| `[abc]` | Any one of a, b, c |
| `[^abc]` | Any character except a, b, c |
| `[a-z]` | Range |

Note `\w` includes the underscore and excludes accented letters. For international text use `\p{L}` with the `u` flag.

## Quantifiers

| Pattern | Meaning |
|---|---|
| `*` | Zero or more |
| `+` | One or more |
| `?` | Zero or one |
| `{3}` | Exactly three |
| `{2,}` | Two or more |
| `{2,5}` | Two to five |
| `*?` `+?` | Lazy - match as few as possible |

Quantifiers are greedy by default. `<.+>` against `<a><b>` matches the whole string; `<.+?>` matches just `<a>`.

## Anchors and boundaries

| Pattern | Matches |
|---|---|
| `^` | Start of string, or line with `m` flag |
| `$` | End of string, or line with `m` flag |
| `\b` | Word boundary |
| `\B` | Not a word boundary |

`\b` is the one most people underuse. `\bcat\b` matches "cat" but not "category" - usually what you meant.

## Groups

```js
/(\d{4})-(\d{2})/          // numbered groups
/(?<year>\d{4})-(?<month>\d{2})/  // named groups
/(?:https?):\/\//          // non-capturing
```

Named groups land in `match.groups.year`, which is far more readable than `match[1]` six months later. Use non-capturing groups whenever you group only for alternation - it makes the intent clear and is marginally faster.

## Lookaround

```js
/foo(?=bar)/    // foo followed by bar
/foo(?!bar)/    // foo not followed by bar
/(?<=\$)\d+/    // digits preceded by $
/(?<!\$)\d+/    // digits not preceded by $
```

Lookahead is universal. Lookbehind is supported in modern JavaScript engines but not in Safari before 16.4 and not in some other languages - check before shipping it to browsers.

Free tool: [Regex Tester](https://rankcert.com/tools/regex-tester) - Test a JavaScript regular expression against sample text, see every match with its position and capture groups, and get real errors for invalid patterns.

## Flags

| Flag | Effect |
|---|---|
| `g` | Find all matches, not just the first |
| `i` | Case insensitive |
| `m` | `^` and `$` match line boundaries |
| `s` | `.` matches newlines |
| `u` | Unicode mode - required for `\p{...}` |
| `y` | Sticky - match only at `lastIndex` |

## JavaScript-specific traps

**`lastIndex` on global regexes is stateful.** A regex literal with `g` reused across calls to `.test()` remembers where it stopped:

```js
const re = /a/g;
re.test("a");  // true
re.test("a");  // false - lastIndex is 1
```

Create the regex inside the function, or reset `lastIndex` explicitly. This bug is subtle because it only appears on the second call.

**`String.match` behaves differently with `g`.** Without it you get capture groups; with it you get an array of full matches and no groups. Use `matchAll` when you want both.

**Escaping in `new RegExp`.** The argument is a string, so backslashes need doubling: `new RegExp("\\d+")`. Prefer literals when the pattern is static.

## Catastrophic backtracking

The one regex bug that takes down a server.

```js
/(a+)+$/.test("aaaaaaaaaaaaaaaaaaaaaaaaaaX")
```

Nested quantifiers over the same character create exponentially many ways to split the input. On a non-matching string the engine tries all of them, and the page hangs.

The tells: a quantifier applied to a group that itself contains a quantifier, or alternation where branches can match the same text - `(a|a)*`.

The fix is to remove the ambiguity. `(a+)+` should be `a+`. `(\w+\s?)+` should be `[\w\s]+`. If you cannot make it unambiguous, do not use a regex.

<Callout>
Validate emails with `/^[^@\s]+@[^@\s]+\.[^@\s]+$/` and send a confirmation link. RFC 5322-compliant email regexes are hundreds of characters long, still reject valid addresses, and prove nothing about whether the mailbox exists.
</Callout>

<Cta />
