EDT
Everyday Dev Tools
Fast tools, no fuss
Home / Tutorials / Regex for Beginners: A Comprehensive Practical Tutorial
Regex

Regex for Beginners: A Comprehensive Practical Tutorial

2026-06-20 · 20 min read

Learn regular expressions basics with real-world examples, common patterns, and practical advice from years of debugging regex.

What is Regex, and Why Should You Learn It?

Regex (short for regular expressions) is a powerful pattern-matching language for text. It lets you define what you're looking for in abstract terms, and the regex engine will find matches for you.

Let me start with a story. Early in my career, I had to extract 500+ email addresses from a messy text file that was a copy-paste from a web page. I spent 30 minutes trying to do it manually, then a senior developer showed me a regex: `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`. It extracted every email in 2 seconds, and I felt like I'd just witnessed magic.

Another example: `\d+` matches one or more digits. That's it — just two characters, but it will find "123", "4567", "8", and any other sequence of digits in your text.

Regex looks cryptic at first — like someone fell asleep on their keyboard. But you can learn the basics in an afternoon, and those basics will solve 80% of your text-processing problems. I still use regex almost daily for log parsing, data extraction, validation, and quick find-and-replace operations.

That said, regex is a tool, not a solution to everything. It's powerful, but it can also become unreadable and hard to maintain if you overcomplicate it. We'll talk about that balance too.

The Fundamentals: Literal Characters and Simple Matches

Let's start with the simplest regex patterns: literal characters. If you just type characters normally, the regex will match that exact sequence.

**Example:** The regex `hello` will match:

- "hello" (exactly)

- "say hello" (contains "hello" somewhere)

- "hello world" (starts with "hello")

But it won't match "Hello" with a capital H, or "helloworld" without the space — it's looking for the exact sequence `h-e-l-l-o`.

Some characters have special meaning in regex, so if you want to match them literally, you need to escape them with a backslash (`\`). These are the special characters: `. * + ? ^ $ { } ( ) [ ] \ | /`

**Examples of escaping:**

- To match a literal period: `\.` (not just `.` — that matches any character!)

- To match a literal question mark: `\?`

- To match a literal backslash: `\\` (yes, two backslashes — the first escapes the second)

- To match parentheses: `\(` and `\)`

This is a common gotcha. I once spent 20 minutes trying to match a URL like "example.com?q=123" with the regex `example.com?q=\d+`, and it wouldn't work because I forgot to escape the `.`, `?`, and `=`. The correct regex is `example\.com\?q=\d+`.

**Pro tip:** When you're just starting out, use our Regex Tester to experiment. It shows you matches in real-time, which makes learning much faster.

Character Classes: The Most Useful Building Blocks

Character classes let you match "one of these types of characters" instead of just literal characters. They're the workhorses of regex — I use these constantly.

**Predefined character classes (shorthand):**

- `\d`: Any digit (0-9) — think "d" for "digit"

- `\w`: Any word character (a-z, A-Z, 0-9, underscore) — think "w" for "word" (note: this includes underscores, which surprises people sometimes)

- `\s`: Any whitespace (space, tab, newline, carriage return, vertical tab) — think "s" for "space"

- `.`: Any character except newline (dot matches almost anything!)

**Negated versions (the opposite):**

- `\D`: Any character that's NOT a digit

- `\W`: Any character that's NOT a word character

- `\S`: Any character that's NOT whitespace

**Examples in action:**

- `\d\d\d` matches three digits in a row, like "123" or "456"

- `\w+` matches a sequence of word characters, like "hello" or "user_id_123"

- `hello\sworld` matches "hello world" with any whitespace between them (space, tab, etc.)

- `a.c` matches "abc", "a1c", "a c", but not "ac" or "a\nc" (because the dot doesn't match newlines by default)

A common mistake: Using `.` when you don't mean to. Remember that `.` matches almost anything! If you mean a literal period, escape it with `\.`.

**Real-world use case:** I once used `\d+` to extract all the order numbers from a log file with thousands of lines. It took 2 seconds, and it would have taken me hours to do manually.

Quantifiers: How Many to Match

Quantifiers let you specify how many times something should match. They come after the character or character class you want to quantify.

**The basic quantifiers:**

- `+`: One or more (at least one) — "greedy" (matches as much as possible)

- `*`: Zero or more (optional, can be many) — also greedy

- `?`: Zero or one (optional, at most one) — makes something optional

- `{3}`: Exactly 3 times

- `{2,5}`: Between 2 and 5 times (inclusive)

- `{2,}`: At least 2 times (no upper limit)

**Examples:**

- `\d+` matches one or more digits: "1", "12", "123", etc.

- `\w*` matches zero or more word characters (note: this will match empty strings too!)

- `colou?r` matches both "color" and "colour" — the `u` is optional

- `\d{3}` matches exactly three digits: "123", but not "12" or "1234"

- `\d{2,4}` matches 2, 3, or 4 digits

**Greedy vs. lazy:** By default, `+` and `*` are greedy — they match as much as possible. If you add a `?` after them, they become lazy (match as little as possible).

**Example with HTML:**

- Greedy: `<.*>` applied to `<b>hello</b>` will match the entire string (greedy takes everything)

- Lazy: `<.*?>` will just match `<b>` (lazy takes the smallest possible match)

This is a common gotcha with HTML/XML — the greedy quantifier will match from the first `<` to the last `>`, which is usually not what you want. Use the lazy version (add a `?`) instead.

**Practical example:** I used `https?://[^\s]+` (note the `s?` making the "s" optional) to extract both HTTP and HTTPS URLs from a document.

Anchors: Match the Start or End of a String

Anchors don't match characters — they match positions. They're crucial for validation, because they let you say "the entire string should match this pattern, not just contain it somewhere."

**The two main anchors:**

- `^`: Start of string (or start of line in multiline mode)

- `$`: End of string (or end of line in multiline mode)

**Examples:**

- `^Hello`: Matches strings that start with "Hello" — "Hello world", "Hello there", but not "Say Hello"

- `world$`: Matches strings that end with "world" — "Hello world", "Goodbye world", but not "world of programming"

- `^Hello world$`: Matches exactly "Hello world" and nothing else — not "Hello world!", not "Hi Hello world", just the exact string

**Why anchors matter for validation:** Let's say you want to validate that a user enters exactly 5 digits. If you use just `\d{5}`, it will match "abc12345def" because it contains 5 digits somewhere. But if you use `^\d{5}$`, it will only match exactly 5 digits and nothing else — that's what you want for validation.

**Real-world story:** I once saw a signup form where the email validation was missing anchors. The regex was something like `.*@.*\..*`, which technically checks for an @ and a dot — but it would also match "[email protected]" because there was no anchor. Someone accidentally pasted extra text, and the form accepted it. Adding `^` and `$` fixed the problem.

**Word boundaries:** There's also `\b` for word boundaries. `\bhello\b` matches "hello" as a whole word, but not "helloworld" or "sayhello". I use this for finding whole words in text.

Custom Character Classes with Brackets

Sometimes the predefined character classes (`\d`, `\w`, etc.) aren't enough. You can define your own character classes with square brackets `[]`.

**Basic custom classes:**

- `[abc]`: Match a, b, or c — just one of them

- `[aeiou]`: Match any vowel (lowercase)

- `[0123456789]`: Same as `\d` — any digit

**Ranges:**

- `[a-z]`: Match any lowercase letter from a to z

- `[A-Z]`: Match any uppercase letter from A to Z

- `[0-9]`: Match any digit

- `[a-zA-Z0-9]`: Match any letter (upper or lower) or digit — same as `\w` but without the underscore

- `[a-fA-F0-9]`: Match hexadecimal digits

**Negated character classes:** Put a `^` as the first character inside the brackets to mean "anything except these."

- `[^abc]`: Match anything except a, b, or c

- `[^0-9]`: Match anything except a digit — same as `\D`

- `[^\s]`: Match anything except whitespace — same as `\S`

**Examples in action:**

- `[0-9]{3}`: Three digits — same as `\d{3}`

- `[a-zA-Z]+`: One or more letters (no numbers, no underscores)

- `[^@]+`: One or more characters that are NOT @ — perfect for the part before the @ in an email

- `[2-9]`: Digits from 2 to 9 (useful for phone numbers that don't start with 0 or 1)

**Common gotchas:** The `^` only negates if it's the first character in the brackets. Otherwise, it's just a literal `^`. Also, most special characters lose their special meaning inside brackets — you don't need to escape them there.

**Practical example:** A simple (but not perfect) email pattern: `[^@]+@[^@]+\.[^@]+` — this says "one or more non-@ characters, then @, then one or more non-@ characters, then a dot, then one or more non-@ characters." It's not RFC-compliant, but it catches most typos.

Practical Example 1: Validate a US Phone Number

Let's build a regex for validating US phone numbers in the format "123-456-7890" step by step. This is a common task, and it shows how to combine the pieces we've learned.

**Step 1: Start with anchors.** We want the entire string to match, not just contain a phone number somewhere:

`^...$`

**Step 2: First three digits.** That's `\d{3}`:

`^\d{3}...$`

**Step 3: Add the first dash.** We need to escape... wait, no, dash isn't special unless it's in a character class. Just literal `-`:

`^\d{3}-...$`

**Step 4: Next three digits and another dash:**

`^\d{3}-\d{3}-...$`

**Step 5: Final four digits and end anchor:**

`^\d{3}-\d{3}-\d{4}$`

That's our regex! Let's test what it matches:

- ✅ "123-456-7890" — perfect match

- ❌ "1234-567-890" — wrong grouping

- ❌ "(123) 456-7890" — we didn't account for parentheses or spaces

- ❌ "123.456.7890" — we didn't account for dots

**Making it more flexible:** If we want to allow different separators (dashes, dots, spaces), we could use `[-.\s]` instead of just `-`. But be careful — if you allow multiple separators, you might want to be consistent about it.

**Real-world advice:** Phone number validation is surprisingly complex. Different countries have different formats, extensions, etc. For most applications, being somewhat permissive is better than being too strict — users get frustrated when their perfectly valid phone number is rejected. I usually normalize the input first (remove all non-digits), then check the length, rather than trying to validate the format directly.

**In code:**

function validatePhone(phone) {
  const regex = /^\d{3}-\d{3}-\d{4}$/;
  return regex.test(phone);
}

console.log(validatePhone('123-456-7890')); // true
console.log(validatePhone('1234-567-890')); // false

Use our Regex Tester to experiment with this pattern — try adding anchors, removing them, and see what matches.

Practical Example 2: Extract URLs from Text

Another common task: extracting URLs from a block of text. This is great for scraping, log analysis, or just finding links in a document.

Let's build a pattern that matches URLs starting with http:// or https://.

**Step 1: The scheme.** We want "http" with an optional "s", then "://":

`https?://`

The `s?` means "optional s" — zero or one s, so this matches both "http://" and "https://".

**Step 2: The rest of the URL.** We want everything until we hit whitespace. Remember, `[^\s]` means "any character except whitespace":

`https?://[^\s]+`

That's it! Let's break it down:

- `http`: Literal "http"

- `s?`: Optional "s"

- `://`: Literal "://"

- `[^\s]+`: One or more characters that are NOT whitespace

**What this matches:**

- ✅ "http://example.com"

- ✅ "https://example.com/path?query=123"

- ✅ "http://subdomain.example.co.uk/page#anchor"

**What it doesn't match:**

- ❌ "example.com" (missing the http:// part)

- ❌ "ftp://example.com" (we only match http/https)

**Is this perfect?** No. It doesn't handle all edge cases — for example, it might include trailing punctuation if the URL is at the end of a sentence. But it's "good enough" for many real-world use cases, especially if you're just extracting URLs from your own documents or logs.

**In JavaScript code:**

function extractUrls(text) {
  const regex = /https?:\/\/[^\s]+/g;
  return text.match(regex) || [];
}

const text = 'Check out https://example.com and http://test.org!';
console.log(extractUrls(text));
// [ 'https://example.com', 'http://test.org' ]

Note the `g` flag at the end — that means "global" and finds all matches, not just the first one. We'll talk more about flags next.

**Pro tip:** For serious URL handling, use a dedicated URL parsing library — regex can only take you so far. But for quick extraction, this pattern works great.

Regex Flags You Should Know

Flags change how the regex engine behaves. In JavaScript, they go after the final slash: `/pattern/flags`. In other languages, they're set differently, but the concepts are similar.

**The three most useful flags:**

**1. `g` — Global**

Finds all matches in the string, not just the first one. Without this flag, `match()` returns just the first match. With this flag, it returns all matches.

Example: `/\d+/g` applied to "123 456 789" returns ["123", "456", "789"].

**2. `i` — Case-insensitive**

Ignores the difference between uppercase and lowercase letters.

Example: `/hello/i` matches "Hello", "HELLO", "hello", "hElLo", etc.

**3. `m` — Multiline**

Changes how `^` and `$` work. Instead of matching just the start and end of the entire string, they match the start and end of each line.

**Examples of using flags together:**

- `/hello/gi` — Find all instances of "hello" case-insensitively

- `/^\d+/gm` — Find lines that start with digits

**Less common flags (good to know about):**

- `s` — Dotall mode (`.` matches newlines too)

- `u` — Unicode mode (better handling of Unicode characters)

- `y` — Sticky mode (match only at lastIndex position)

**Real-world example:** I once used `/^ERROR: .*$/gim` to extract all error lines from a log file, regardless of whether "ERROR" was uppercase, lowercase, or mixed. The `m` flag let `^` match the start of each line, not just the start of the file.

**In the Regex Tester:** You can set these flags in our tool — just check the boxes for the flags you want, and see how the matches change.

Groups: Extracting Parts of Matches

Parentheses let you capture groups of characters, which is incredibly useful for extracting specific parts of a match instead of just the whole thing.

**Basic groups with `()`:**

- Use parentheses to group parts of your pattern

- You can then extract those groups individually

**Example: Extracting name and domain from an email**

Let's say we have an email like "[email protected]" and we want to get "user" and "example.com" separately.

Regex: `^([^@]+)@([^@]+)$`

The parentheses create two groups:

- Group 1: `([^@]+)` — the part before @

- Group 2: `([^@]+)` — the part after @

**In JavaScript:**

const email = '[email protected]';
const regex = /^([^@]+)@([^@]+)$/;
const match = email.match(regex);
if (match) {
  console.log(match[0]); // '[email protected]' (the whole match)
  console.log(match[1]); // 'user' (group 1)
  console.log(match[2]); // 'example.com' (group 2)
}

**Non-capturing groups:** If you want to group something but don't need to capture it, use `(?:...)`. This is useful for grouping without cluttering your match results.

Example: `(?:https?://)?([^/]+)` — the `(?:...)` group doesn't get captured, only the second group does.

**Named groups (modern JavaScript):** You can name groups with `(?<name>...)` for more readable code:

const regex = /^(?<name>[^@]+)@(?<domain>[^@]+)$/;
const match = email.match(regex);
if (match) {
  console.log(match.groups.name);   // 'user'
  console.log(match.groups.domain); // 'example.com'
}

**Real-world use case:** I use groups all the time for parsing log files. A log line like "2026-06-20 14:30:00 [INFO] User 123 logged in" can be parsed with a regex with groups for the timestamp, level, user ID, and message. It makes structured data out of unstructured logs.

Common Regex Pitfalls and How to Avoid Them

Regex is powerful, but it's easy to make mistakes. Here are the pitfalls I see most often, and how to avoid them.

**Pitfall 1: Forgetting to escape special characters.** Remember: `. * + ? ^ $ { } ( ) [ ] \ | /` are special. If you want to match them literally, escape them with `\`. I still do this by accident sometimes — especially with `.`!

**Pitfall 2: Using `.` when you don't mean to.** The dot matches almost anything. If you mean a literal period, use `\.`. If you mean "anything except whitespace", use `[^\s]`.

**Pitfall 3: Regex is too greedy.** Remember that `+` and `*` are greedy by default — they match as much as possible. Add a `?` to make them lazy: `+?` or `*?`. This is a common issue with HTML/XML — `<.*>` matches way too much, but `<.*?>` matches just one tag.

**Pitfall 4: Forgetting anchors for validation.** If you're validating that the entire string matches, use `^` and `$`. Otherwise, you might accept strings that contain the pattern but have extra stuff around it.

**Pitfall 5: Trying to use regex for HTML/XML.** Regex can't properly parse HTML — HTML isn't a regular language. For simple cases like extracting links, it's okay, but for serious HTML parsing, use an HTML parser library instead.

**Pitfall 6: Making regex too complicated.** If your regex is 50 characters long and you can't read it, consider breaking it into smaller pieces, or not using regex at all. Readability matters — you have to maintain this code later!

**Pitfall 7: Not testing enough.** Always test your regex with: good inputs, bad inputs, edge cases, empty strings. Our Regex Tester helps with this — you can test multiple inputs at once.

**My rule of thumb:** If I can't read my own regex the next day, it's too complicated. I either simplify it or add comments explaining what it does.

My Practical Regex Workflow

After using regex for years, this is the workflow I follow to build patterns that work and are maintainable.

**1. Start simple.** Don't try to write the perfect regex on the first try. Start with the simplest pattern that matches the basic case, then refine it.

**2. Test as you go.** Use our Regex Tester or your editor's regex search to test after each change. This is much faster than writing the whole thing first and then debugging why it doesn't work.

**3. Use a real sample.** Test with actual data, not just made-up examples. Real data has edge cases you won't think of.

**4. Test edge cases.** Try empty strings, strings that are too long, strings with special characters, etc.

**5. Keep it readable.** If it gets too complex, break it into parts or use comments. In many regex flavors, you can use the `x` flag to ignore whitespace and add comments, but JavaScript doesn't support that natively. Instead, you can build the regex from strings, or just add a comment explaining what it does.

**6. Consider not using regex.** Sometimes string methods are simpler. For example, in JavaScript, `text.includes('hello')` is better than `/hello/.test(text)`. `text.split(',')` is better than a regex for splitting on commas if you don't need the pattern matching.

**7. Document it.** If you write a non-trivial regex, add a comment explaining what it does. Future you will thank you.

**Example of a documented regex:**

// Matches US phone numbers in format XXX-XXX-XXXX
// Groups 1, 2, 3: the three parts of the number
const phoneRegex = /^(\d{3})-(\d{3})-(\d{4})$/;

Regex is a tool — use it when it's the right tool, and don't overcomplicate it. And always use a tester to experiment!

Tools

Try These Tools

Open the tools that go with this tutorial and practice what you learned.

Navigation

Keep Learning