Skip to content
Blogs - jrtilak.dev

Regex Made Easy (Well, Sort of!)

Edit page

Regular expressions can look like someone dropped a box of punctuation onto the keyboard and decided it was code. They are confusing at first, but underneath all those slashes, brackets, and question marks is a small language with a manageable set of rules.

This guide uses JavaScript regex syntax. Many ideas also apply to other languages, but each regex engine has a few rules of its own.

What’s a Regex, Anyway?

In the simplest terms, a regular expression, usually shortened to regex, is a sequence of characters that defines a search pattern. You can think of it as a mini-language for searching, replacing, validating, and extracting text in a flexible way.

For example:

// Find every occurrence of "hello"
const regex = /hello/g;

console.log("hello there, hello again".match(regex));
// ["hello", "hello"]

A normal string search asks for one exact value. A regex can ask for broader patterns, such as “one or more digits,” “a word at the start of a line,” or “either color or colour.” That is where its strange-looking syntax starts earning its keep.

Anatomy of a Basic Regex

In JavaScript, a regex literal usually consists of three main parts:

  1. Delimiters: Forward slashes / that enclose the pattern.
  2. Pattern: The sequence of characters defining what you want to match.
  3. Flags: Optional letters that modify how matching behaves.

Example:

/hello/gi

A semicolon after /hello/gi; belongs to the JavaScript statement, not to the regex.

When a pattern must be created dynamically, JavaScript also provides the RegExp constructor:

const regex = new RegExp("\\d+", "g");

Because the pattern is inside a JavaScript string, its backslash must be escaped. The equivalent regex literal is simply /\d+/g. Yes, regex sometimes makes you escape the escape. Nobody said the mini-language would be emotionally supportive.

Meta-Characters in Regex

Most characters match themselves, but metacharacters have special meaning.

1. . (Dot)

The dot matches any single character except line terminators by default.

/c.t/.test("cat"); // true
/c.t/.test("cot"); // true
/c.t/.test("cut"); // true
/c.t/.test("coat"); // false

With the s flag, the dot can match line terminators too.

2. ^ (Caret)

The caret asserts the start of a string, or the start of a line when multiline mode is enabled.

/^cat/.test("cat naps"); // true
/^cat/.test("a cat naps"); // false

3. $ (Dollar Sign)

The dollar sign asserts the end of a string, or the end of a line in multiline mode.

/cat$/.test("copycat"); // true
/cat$/.test("cat naps"); // false

Use both anchors when the whole input must match:

/^cat$/.test("cat"); // true
/^cat$/.test("cat naps"); // false

4. * (Asterisk)

The asterisk matches the preceding element zero or more times.

/ca*t/.test("ct"); // true
/ca*t/.test("cat"); // true
/ca*t/.test("caaat"); // true

5. + (Plus)

The plus sign matches the preceding element one or more times.

/ca+t/.test("cat"); // true
/ca+t/.test("caaat"); // true
/ca+t/.test("ct"); // false

6. ? (Question Mark)

The question mark makes the preceding element optional by matching it zero or one time.

/colou?r/.test("color"); // true
/colou?r/.test("colour"); // true

It can also make another quantifier lazy, meaning it consumes as little text as possible:

const text = 'He said "hello" and then "goodbye".';

console.log(text.match(/".*"/g));
// ['"hello" and then "goodbye"']

console.log(text.match(/".*?"/g));
// ['"hello"', '"goodbye"']

7. {} (Curly Braces)

Curly braces specify how many times the preceding element should appear.

/a{3}/.test("aaa"); // exactly three
/a{3,}/.test("aaaaa"); // at least three
/a{3,6}/.test("aaaa"); // between three and six

JavaScript supports {n}, {n,}, and {n,m}. It does not support {,m}, so use something like {0,6} when the lower bound is zero.

8. [] (Square Brackets)

Square brackets define a character class. The class matches one character from the set.

/[aeiou]/.test("cloud"); // true
/[aeiou]/.test("sky"); // false

9. | (Pipe)

The pipe acts as an OR operator.

/cat|dog/.test("I have a dog"); // true

Alternation has low precedence, so grouping is important when other rules must apply to every option:

/^cat|dog$/.test("cat naps"); // true, probably not intended
/^(?:cat|dog)$/.test("cat naps"); // false

10. () (Parentheses)

Parentheses group patterns and, by default, capture the matched content.

/(cat|dog)/.exec("I have a dog");
// captures "dog"

11. \ (Backslash)

The backslash either escapes a special character or starts a special sequence.

/\./.test("version 1.0"); // matches a literal dot
/\$10/.test("Price: $10"); // matches a literal dollar sign

Some Regex Shorthand Character Classes

Regex provides shortcuts for common character sets.

1. \d

/\d+/.exec("Room 204"); // matches "204"

2. \D

/\D+/.exec("abc123"); // matches "abc"

3. \w

/\w+/.exec("hello_world123"); // matches "hello_world123"

4. \W

/\W+/.exec("hello, world"); // matches ", "

5. \s

/\s+/.test("hello world"); // true

6. \S

/\S+/.exec("  hello world"); // matches "hello"

7. \b

/\bcat\b/.test("a cat sleeps"); // true
/\bcat\b/.test("caterpillar"); // false

8. \B

/\Bcat/.test("bobcat"); // true
/\Bcat/.test("cat"); // false

JavaScript’s \w, \b, and \B are mostly ASCII-oriented. For international text, Unicode property escapes are often better:

/^\p{L}+$/u.test("東京"); // true: Unicode letters
/^[\p{L}\p{M}]+$/u.test("नमस्ते"); // true: letters and combining marks

What About \A and \Z?

Some regex engines use \A for the absolute beginning of a string and \Z for its end. JavaScript does not support either one. Use ^ and $, and add the m flag only when those anchors should work per line.

Capture Groups in Regex

Capture groups let you extract and reuse specific parts of matched text. They are defined with parentheses () and are useful for grouping, parsing, and replacement.

How to Use Capture Groups

Creating a Capture Group

Place the pattern you want to capture inside parentheses:

const match = /(cat)/.exec("a cat sleeps");

console.log(match?.[1]); // "cat"

The complete match is stored at index 0, and the first captured group is stored at index 1.

Using Multiple Capture Groups

Groups are numbered from left to right by the position of their opening parenthesis.

const match = /(cat) and (dog)/.exec("cat and dog");

console.log(match?.[1]); // "cat"
console.log(match?.[2]); // "dog"

Non-Capturing Groups

Sometimes you need to group alternatives or apply a quantifier but do not need the result in the captured output. Use (?:...):

/(?:cat|dog) house/.test("dog house"); // true

The group still organizes the pattern, but it does not add another numbered capture.

Converting a Capture Group to a Non-Capturing Group

Add ?: immediately after the opening parenthesis.

// Capturing group
/(cat|dog)/

// Non-capturing group
/(?:cat|dog)/

Use capturing groups when you need the matched part later. Otherwise, non-capturing groups keep the result and numbering easier to understand.

Naming Capture Groups

Named groups use (?<name>...). They make extracted data much clearer than remembering whether the year was group two or group three.

const pattern = /^(?<day>\d{2})-(?<month>\d{2})-(?<year>\d{4})$/;
const match = pattern.exec("23-09-2024");

if (match?.groups) {
  console.log(match.groups.day); // "23"
  console.log(match.groups.month); // "09"
  console.log(match.groups.year); // "2024"
}

Named groups are also useful during replacement:

const result = "23-09-2024".replace(
  pattern,
  "$<year>-$<month>-$<day>"
);

console.log(result); // "2024-09-23"

Captured groups can even be referenced inside the regex. This pattern finds a repeated word:

const repeatedWord = /\b(\w+)\s+\1\b/gi;

console.log("This is is repeated".match(repeatedWord));
// ["is is"]

Character Classes and Negation in Regex

Character classes specify a set of possible characters. A class matches one character from that set, while a negated class matches one character outside it.

Character Classes

1. Basic Character Class

/[abc]/

2. Character Ranges

/[a-z]/

3. Multiple Ranges

/[A-Za-z0-9]/

4. Combining Characters and Ranges

/[aeiou0-9]/

Inside a character class, most metacharacters lose their special meaning. For example, . means a literal dot. A hyphen creates a range when placed between characters; put it first or last, or escape it, when you need a literal hyphen.

/[.-]/.test("file-name"); // true

Negated Character Classes

Place ^ immediately after [ to match characters that are not in the set.

/[^a-z]/

The caret negates only when it appears at the beginning of the class. In [a^b], it is just a literal caret.

Common Use Cases for Negation

1. Matching Non-Digit Characters

/[^0-9]+/.exec("abc123"); // matches "abc"

2. Matching Non-Whitespace Characters

/[^\s]+/.exec("hello world"); // matches "hello"

This is equivalent to using \S+.

3. Matching Non-Alphanumeric Characters

/[^A-Za-z0-9]+/.exec("hello@#!"); // matches "@#!"

Using Character Classes with Quantifiers

Character classes become more useful when combined with quantifiers.

/[^0-9]+/.exec("abc123XYZ"); // matches "abc"
/[^A-Za-z]+/.exec("hello123!"); // matches "123!"
/[A-F0-9]{6}/.test("7C3AED"); // true

The class chooses what one character may be; the quantifier decides how many of those characters may appear.

Commonly Used Regex Flags

Flags modify regex behavior. JavaScript supports several flags, but the following are the ones beginners will use most often.

1. i (Ignore Case)

Makes matching case-insensitive.

"Hello HELLO hello".match(/hello/gi);
// ["Hello", "HELLO", "hello"]

2. g (Global)

Finds all matches instead of only the first.

"cat and catalog and catnip".match(/cat/g);
// ["cat", "cat", "cat"]

3. m (Multiline)

Makes ^ and $ work at the start and end of each line. Use it with g when you want every matching line.

const input = "test line 1\nanother line\ntest line 2";

console.log(input.match(/^test/gm));
// ["test", "test"]

4. s (Dot All)

Allows . to match line terminators.

/hello.world/s.test("hello\nworld"); // true

5. u (Unicode)

Enables Unicode-aware parsing and code-point behavior, including Unicode property escapes.

/^\p{L}+$/u.test("こんにちは"); // true

6. x (Extended)

The original draft included the x flag because some regex engines support readable whitespace and comments inside patterns. JavaScript does not support it.

If a JavaScript regex becomes complicated, split it into documented pieces or explain each part beside the code instead of pasting an x-flag pattern that will throw a syntax error.

JavaScript also has advanced flags such as d, v, and y, but g, i, m, s, and u cover most everyday work.

Putting Regex to Work in JavaScript

JavaScript provides a few common ways to use a regex.

Test Whether a Match Exists

const hasError = /error/i.test("Database ERROR");
console.log(hasError); // true

Find Matches

const words = "one, two, three".match(/\w+/g);
console.log(words); // ["one", "two", "three"]

Use matchAll() when you need all matches and their capture groups:

const input = "name=Tilak role=developer";
const pairs = [...input.matchAll(/(?<key>\w+)=(?<value>\w+)/g)];

for (const pair of pairs) {
  console.log(pair.groups);
}

Search and Replace

const cleaned = "too     many   spaces".replace(/\s+/g, " ");
console.log(cleaned); // "too many spaces"

Split Text

const values = "red, green;blue | yellow".split(/\s*[,;|]\s*/);
console.log(values); // ["red", "green", "blue", "yellow"]

Common Use Cases of Regex

  1. Data validation: Checking whether input has an expected shape, such as a date, username, or color code.
  2. Search and replace: Finding repeated spaces, formatting text, or replacing captured values.
  3. Text parsing: Extracting controlled values such as hashtags, IDs, timestamps, or log levels.
  4. Log analysis: Finding error messages, request IDs, IP-like values, or timestamps in predictable log formats.
  5. Web scraping: Matching narrow, controlled text fragments. For arbitrary HTML, use DOMParser or a proper HTML parser; HTML has enough edge cases to make a heroic regex regret its career choices.

Commonly Used Regex Patterns

These patterns are useful starting points, not universal validators.

1. Simple Email Shape

const simpleEmailShape = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

This catches common formatting mistakes, but it is not a complete implementation of every valid email address. Real email verification still requires sending a verification message.

2. One Specific Phone Number Shape

const phoneShape = /^(?:\+[1-9]\d{0,2}[-. ]?)?\d{3}[-. ]?\d{3}[-. ]?\d{4}$/;

This demonstrates optional country codes and separators for one narrow structure. International phone numbers vary widely, so use a library such as libphonenumber for production validation.

3. URL Validation

URLs have more rules than a short regex usually handles. JavaScript already has a parser:

function isHttpUrl(value) {
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

Sometimes the best regex is no regex. Character growth achieved.

4. Date Format (YYYY-MM-DD)

const isoDateShape = /^\d{4}-\d{2}-\d{2}$/;

This checks the shape only. It still accepts impossible dates such as 2026-99-99, so parse the date when calendar validity matters.

5. Hexadecimal Color Code

const hexColor = /^#?(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;

It accepts three- or six-digit hexadecimal colors, with an optional leading #.

6. Extract Unicode Hashtags

const post = "Learning #JavaScript with #नमस्ते and #東京";
const hashtags = post.match(/#[\p{L}\p{M}\p{N}_]+/gu);

console.log(hashtags);
// ["#JavaScript", "#नमस्ते", "#東京"]

7. Parse a Controlled Log Entry

const logPattern =
  /^\[(?<time>\d{2}:\d{2}:\d{2})\] (?<level>INFO|WARN|ERROR): (?<message>.+)$/;

const entry = logPattern.exec("[14:32:08] ERROR: Connection timed out");

console.log(entry?.groups);
// { time: "14:32:08", level: "ERROR", message: "Connection timed out" }

A Few Regex Traps Worth Knowing

Global Patterns Remember Their Position

Patterns using g or y update their lastIndex when used with test() or exec().

const pattern = /cat/g;

pattern.test("cat"); // true
pattern.test("cat"); // false: matching resumed after the first result

pattern.lastIndex = 0;

Remove g when you need only a yes-or-no test.

Escape Dynamic User Input

User text may contain regex syntax such as ., *, or (. Modern JavaScript provides RegExp.escape() for turning it into a literal pattern safely.

const searchText = "file.js";
const pattern = new RegExp(RegExp.escape(searchText), "gi");

pattern.test("Open FILE.JS"); // true

Avoid Dangerous Backtracking

Complex nested quantifiers can take a very long time on carefully chosen input, creating a regular expression denial-of-service problem. Keep patterns understandable, test long near-matches, and never execute arbitrary user-provided regex without safeguards.

Conclusion

Regular expressions are the Swiss Army knife of text processing: versatile, powerful, and slightly confusing when you first unfold all the tools. They can validate a format, search logs, extract data, replace text, and turn repetitive string work into a small pattern.

Start with literal text. Add character classes, quantifiers, anchors, and groups one piece at a time. Test values that should match, values that should fail, and the awkward edge cases hiding between them. Also remember that regex checks patterns, not truth: 2026-99-99 can look like a date while being absolutely useless to a calendar.

Embrace the quirks, keep a regex tester nearby, and soon the mysterious wall of punctuation will start looking like a useful tool instead of an encrypted cry for help.

For a complete syntax reference, see MDN’s JavaScript regular expressions guide and character class reference.


Edit page
Share this post:

Previous Post
Vehify: How the AI Sales Assistant Worked
Next Post
Vivid Tab: Building the New Tab I Wanted to Use