# Regex from zero: ten patterns and the trap that freezes pages

> A regular expression looks like a cat walked across the keyboard. It is really a tiny language for describing text. Learn six symbols and you can check a PIN code, pull numbers out of a paragraph or tidy a messy list. Learn one warning and you will not freeze your own page.

Published 2026-10-09 by Toolcloak. 4 minute read. Original: https://toolcloak.pages.dev/articles/regex-from-zero-ten-patterns-and-the-trap-that-freezes-pages
Related free tool: [Regex Tester](https://toolcloak.pages.dev/tools/regex-tester)

A **regular expression**, or regex, is a pattern that describes a piece of text. Instead of asking 'is this exactly the word cat?', you ask 'does this look like six digits?' or 'is there a date somewhere in here?'. Almost every programming language and text editor understands the same core ideas.

> **In short**
> - Six building blocks cover most uses: `\d` (a digit), `\w` (a letter, digit or underscore), `.` (any character), `[ ]` (one of these), `+` / `*` / `?` / `{n}` (how many), and `^` / `$` (start and end).
> - Round brackets **capture** parts of a match so you can reuse them.
> - By default quantifiers are **greedy**: they take as much as they can. Add `?` after them to make them lazy.
> - Nested repeats like `(a+)+` can make a regex take forever on some inputs. Keep patterns simple.

## Anatomy of a pattern

Take the pattern `^[6-9]\d{9}$`, a simple check for a 10-digit Indian mobile number. It reads left to right, one piece at a time:

**Reading ^[6-9]\d{9}$ in plain words**

1. **^.** The match must start at the very beginning of the text.
2. **[6-9].** The first character is one of 6, 7, 8 or 9.
3. **\d{9}.** Followed by exactly nine more digits.
4. **$.** And then the text must end. Nothing extra is allowed.

*Without ^ and $, the pattern would also match inside a longer string such as 1234567890123. Anchors turn 'contains' into 'is exactly'.*

Try it: [Regex Tester](https://toolcloak.pages.dev/tools/regex-tester). Type a pattern and some text. Matches are highlighted live, with each capture group listed.

## The symbols you will use daily

| Symbol | Meaning | Example |
| --- | --- | --- |
| `\d` | One digit, 0 to 9 | `\d\d` matches 42 |
| `\w` | A letter, digit or underscore | `\w+` matches user_1 |
| `\s` | A space, tab or line break | `\s+` matches a gap of spaces |
| `.` | Any one character except a new line | `c.t` matches cat and cot |
| `[abc]` | One of the listed characters | `gr[ae]y` matches gray and grey |
| `[^abc]` | Any character not listed | `[^0-9]` matches a non-digit |
| `+` | One or more | `a+` matches a, aa, aaa |
| `*` | Zero or more | `ab*` matches a, ab, abb |
| `?` | Zero or one (optional) | `colou?r` matches color and colour |
| `{n,m}` | Between n and m times | `\d{2,4}` matches 12, 123 and 1234 |
| `( )` | A group, captured for reuse | `(\d+)-(\d+)` captures both numbers |
| `a|b` | Either a or b | `cat|dog` matches either word |

*In most languages a backslash inside a normal string has to be doubled, so \d is written \\d in code. In the tester you type it once.*

## Ten patterns worth keeping

| Goal | Pattern |
| --- | --- |
| A whole number | `^\d+$` |
| Indian PIN code (6 digits, not starting with 0) | `^[1-9][0-9]{5}$` |
| Indian mobile number | `^[6-9]\d{9}$` |
| A date in the shape 2026-10-09 | `^\d{4}-\d{2}-\d{2}$` |
| A hex colour like #1a2b3c or #abc | `^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$` |
| A rough email check | `^[^\s@]+@[^\s@]+\.[^\s@]+$` |
| Extra spaces at either end | `^\s+|\s+$` |
| Two or more spaces in a row | `\s{2,}` |
| Every number inside a paragraph | `\d+(\.\d+)?` |
| A repeated word such as 'the the' | `\b(\w+)\s+\1\b` |

*The date pattern checks the shape only: it accepts 2026-99-99. The email pattern is a quick sanity check, not full validation. For real validation, confirm by sending a message.*

## Capture groups: keep the parts you care about

Round brackets mark parts of the match you want to keep. Try `(\d{4})-(\d{2})-(\d{2})` on the text 2026-10-09. You get the whole match, plus three groups: **2026**, **10** and **09**. In JavaScript you can name them with `(?<year>\d{4})` and read them back by name, which makes longer patterns far easier to follow.

**One match, four pieces**

|  | Value | Note |
| --- | --- | --- |
| Whole match | 2026-10-09 | The full text found |
| Group 1 | 2026 | The year |
| Group 2 | 10 | The month |
| Group 3 | 09 | The day |

*Groups are numbered by their opening bracket, from left to right.*

## Greedy or lazy?

Quantifiers like `+` and `*` are **greedy**: they grab as much text as they can and only give some back if the rest of the pattern needs it. That is often not what you want. Take the text `<b>one</b> and <b>two</b>`:

**Same text, two behaviours**

| Greedy: <b>.*</b> | Lazy: <b>.*?</b> |
| --- | --- |
| 1 match | 2 matches |
| Runs to the last </b> | Stops at the first </b> |
| Swallows everything in between | Finds each bold word separately |
| Result: <b>one</b> and <b>two</b> | Results: <b>one</b> and <b>two</b> |

*Adding ? after a quantifier makes it lazy. Remember this one: it is behind a huge share of 'my regex matched too much' questions.*

## Flags change how the whole pattern behaves

**The four flags in the tester**

| Box | What it means |
| --- | --- |
| g (global) | Find every match, not just the first one. |
| i (ignore case) | Treat A and a as the same letter. |
| m (multiline) | Make ^ and $ match at the start and end of each line. |
| s (dot all) | Let the dot match line breaks as well. |

*Most 'it works on one line but not on my whole text' problems are solved by the m flag.*

## The trap: catastrophic backtracking

When a regex cannot match, the engine does not give up at once. It tries other ways of splitting the text among the quantifiers. With a pattern such as `(a+)+b` run against a long line of a's with no b at the end, the number of ways grows by about **double with every extra character**. A web page that runs such a pattern on user input can freeze its tab, or a server can spend its whole time on one request.

**Rough number of steps for (a+)+b on a string of n a's with no b**

|  | Steps |
| --- | --- |
| n = 10 | about 1 thousand |
| n = 20 | about 1 million |
| n = 25 | about 34 million |
| n = 30 | about 1 billion |

*The count is roughly 2 to the power n. Real engines differ in details and some have protections, but you should never depend on that.*

> **How to stay safe:** Avoid a repeat inside a repeat, such as `(a+)+`, `(.*)*` or `(\w+\s?)*`. Make alternatives clearly different from each other. Limit lengths with `{1,50}` where you can. And if the pattern runs on text a stranger sent you, cap the input length first.

## A good way to learn

1. **Start tiny.** Match one letter, then add one idea at a time.
2. **Test on good and bad text.** Always include at least one example that should fail.
3. **Anchor when you validate.** Use ^ and $ to check a whole value.
4. **Prefer simple patterns.** If it takes you a minute to read, it will take your teammate longer. Add a comment where you use it.
5. **Know when not to use regex.** For full HTML, JSON or email parsing, use a proper parser.

## Frequently asked questions

### What is a regular expression?

A regular expression, or regex, is a pattern that describes a set of strings. You use it to search, check or replace text, for example finding every number in a paragraph or checking that a value looks like a PIN code.

### What does \d mean in regex?

\d matches one digit from 0 to 9. \d+ matches one or more digits and \d{6} matches exactly six digits.

### What is the difference between greedy and lazy matching?

A greedy quantifier such as .* matches as much as it can. A lazy one such as .*? matches as little as it can. On <b>one</b> and <b>two</b>, <b>.*</b> finds one long match and <b>.*?</b> finds two short ones.

### What do ^ and $ do?

^ matches the start of the text and $ matches the end. Using both around a pattern means the whole text must match, not just a part of it.

### What is catastrophic backtracking?

It happens when a pattern with nested repeats, such as (a+)+b, fails on a long input and the engine tries an exponential number of ways to split the text. It can make a page or server hang.

