Back to Blog RSS
Jan 30, 2026
16 min read

Cutting Through JavaScript RegEx 🍕

🚫🍍 /[^pineapple]/

javascript regex tutorial
Cutting Through JavaScript RegEx 🍕

It's been a while since my last post. I've gotten back into doing coding challenges recently and although I'm not an expert on regular expressions by any means, I assumed that everyone knew about them. I've been hosting sessions every night on Discord where people meet up to do coding challenges live and much to my surprise many people had little to no experience with them.

I get it, regular expressions look intimidating at first glance. They look like someone smashed their keyboard and called it code. But once you understand the syntax, they're actually pretty logical and useful.

So what are regular expressions? They're a way to define patterns in text. You can use them to search for specific sequences of characters, validate input, or transform strings based on rules you define. Regular expressions exist in many programming languages, but we're going to focus on how they work in JavaScript.

In the real world, developers use regex for things like validating email addresses and passwords, finding and replacing text in documents, parsing large amounts of data, and cleaning up user input before it hits your database. I would have been at my wits' end without regular expressions while parsing data in YP Scraper and the built-in scraper for DevLeads.

The key to understanding regular expressions is recognizing that they're all about patterns. Once you know how to describe the pattern you're looking for, the rest falls into place. In this post, I'm going to walk you through how to build those patterns using the most common syntax and features you'll actually use. Ready? Let's go.

Creating Regular Expressions

A regular expression in JavaScript is created using forward slashes with your pattern in between. It looks like this: /pattern/. You can also create one using the RegExp constructor like new RegExp('pattern'), which is useful when you need to build a pattern dynamically. For example, if you're getting a search term from user input or an API response and need to create a regex from that returned value, the constructor lets you pass in variables. For most cases, though, you'll use the literal notation with slashes because it's cleaner and easier to read.

Literal Patterns

The simplest pattern you can match is literal text. If you want to find the word "pizza" in a string, your regex is just /pizza/. It matches exactly what you write. So /sauce/ matches "sauce", /CHEESE/ matches "CHEESE", and so on. This works great when you know exactly what you're looking for, but things get more interesting when you need flexibility.

The OR Operator

That's where the OR operator comes in. The pipe symbol | lets you match one pattern or another. So /thin-crust|deep-dish/ will match either "thin-crust" or "deep-dish" in your string. You can chain as many options as you need, like /mozzarella|provolone|parmesan|ricotta/ to match any of those cheeses. This is handy when you have a few specific variations you want to catch.

Character Classes

Character classes give you even more flexibility. When you put characters inside square brackets like [aeiou], it matches any single character from that set. So /[aeiou]/ matches any vowel, and /[0-9]/ matches any single digit. You can also use ranges with a hyphen. /[a-z]/ matches any lowercase letter, /[A-Z]/ matches any uppercase letter, and /[0-9]/ matches any digit from zero to nine. You can combine ranges too, like /[a-zA-Z0-9]/ to match any letter or digit regardless of case.

Negative Character Classes

You can invert a character class by putting an up-caret ^ right after the opening bracket. So [^aeiou] matches any character except vowels. Notice that the up-caret here means "NOT" because it's inside the brackets. This is important because the same ^ symbol means something completely different when it's outside brackets, which we'll get to in a bit. So [^0-9] matches any character that isn't a digit, and [^.,!?] matches any character that isn't common punctuation.

Escape Sequences

Escape sequences are shorthand for common character classes. Instead of writing [0-9] every time you want to match a digit, you can use \d. Instead of [a-zA-Z0-9_] for all word characters (alphanumeric characters plus the underscore '_'), you use \w. And \s matches any whitespace character like spaces, tabs, carriage returns, or newlines. Each of these has an inverse too. \D matches anything that's not a digit, \W matches anything that's not a word character, and \S matches anything that's not whitespace. There are individual escape sequences like \n for newlines, \t for tabs, and \r for carriage returns, but the digit, word, and whitespace shortcuts are the ones you'll reach for most often.

Escape Characters

You can also escape special regex characters that have special meanings. For example, if you want to match a literal dot or asterisk, use \. and \* instead of . and *. This tells the regex engine to treat them as literal characters, not special operators. /\$\d+/ matches "$" followed by digits, /dominoes\.com/ matches "dominoes.com", etc.

Quantifiers

Quantifiers let you specify how many times a pattern should repeat. The asterisk * matches zero or more occurrences, so /z*/ matches zero or more z's in "pizza". The plus sign + matches one or more, so /z+/ requires at least one z. The question mark ? makes something optional by matching zero or one occurrence. You can also be specific with curly braces. /\d{3}/ matches exactly three digits (like an area code), /\d{2,4}/ matches between two and four digits, and /\d{2,}/ matches two or more digits. These quantifiers apply to whatever comes immediately before them, whether that's a single character, a character class, or a group.

By default, quantifiers are greedy. They match as much as possible. If you have the string "<span>Large $18</span><span>Medium $14</span>" and use the pattern /<span>.*<\/span>/, it matches the entire string from the first opening tag to the last closing tag, not just the first span. This happens because .* grabs everything it can. You can make a quantifier lazy by adding a question mark after it. So /<span>.*?<\/span>/ matches as little as possible, stopping at the first closing tag it finds. The lazy version matches "<span>Large $18</span>" and then "<span>Medium $14</span>" separately instead of treating them as one big match.

const greedy = /<span>.*<\/span>/;
const lazy = /<span>.*?<\/span>/;
const menu = "<span>Large $18</span><span>Medium $14</span>";

menu.match(greedy); // ["<span>Large $18</span><span>Medium $14</span>"]
menu.match(lazy); // ["<span>Large $18</span>"]

Anchors

Anchors let you match positions in a string rather than characters. The up-caret ^ matches the start of a string, and the bling symbol $ matches the end. So /^Large/ only matches "Large" if it's at the beginning of the string, and /pizza$/ only matches "pizza" if it's at the end. You can combine them like /^Special: .+ pizza$/ to match lines that start with "Special:" and end with "pizza". Remember, this up-caret is outside the brackets, so it means "start of string" here, not "NOT" like it does inside character classes.

Capture Groups

Parentheses create capture groups, which do two things. First, they let you group parts of your pattern together so you can apply quantifiers to the whole group. Second, they capture the matched text so you can reference it later. So /(yum)+/ matches "yum", "yumyum", "yumyumyum" and so on. We'll see how to use these captured groups with methods like replace() later on.

You can also reference captured groups within the regex itself using backreferences. The syntax \1 refers to the first capture group, \2 to the second, and so on. This is useful when you need to match repeated patterns.

const pattern = /(\w+)\s+\1/;
pattern.test("pizza pizza"); // true
pattern.test("pizza pasta"); // false

You can also name your capture groups using the syntax (?<name>pattern). This makes your regex more readable and lets you reference groups by name instead of by number. For backreferences with named groups, use \k<name>.

const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

Sometimes you want the grouping but don't need to capture the text. That's where non-capturing groups come in. You write them as (?:pattern). So (?:https?) groups the pattern for optional matching but doesn't save the match for later use.

Lookaheads and Lookbehinds

Lookaheads and lookbehinds let you match a pattern only if it's followed or preceded by another pattern, without including that other pattern in the match.

A positive lookahead (?=pattern) checks that what comes next matches the pattern. So /\d+(?=oz)/ matches a digit only if it's followed by "oz", but the "oz" itself isn't part of the match.

A negative lookahead (?!pattern) does the opposite, matching only if what comes next doesn't match the pattern. Lookbehinds work the same way but check what comes before.

A positive lookbehind (?<=pattern) matches only if preceded by the pattern, and a negative lookbehind (?<!pattern) matches only if not preceded by it. These are useful when you need to match something based on context without consuming the surrounding characters.

Flags

Flags modify how the entire regex behaves. You add them after the closing slash. The g flag stands for global and makes the pattern match all occurrences in a string instead of stopping after the first one.

The i flag makes the match case-insensitive, so /pizza/i matches "pizza", "Pizza", "PIZZA", and any other variation.

The m flag is for multiline mode, which changes how the ^ and $ anchors work. Instead of matching only the start and end of the entire string, they match the start and end of each line. So /^Special:/m will match "Special:" at the beginning of any line in a multiline string, not just the very first line.

The s flag enables dotAll mode, which makes the dot . match newline characters in addition to everything else. Normally . matches any character except newlines, so /First.*Second/ wouldn't match across lines. With the s flag, /First.*Second/s will match even if "First" and "Second" are on different lines.

You can combine flags like /pattern/gims to use multiple at once.

Using Regular Expressions with JavaScript Methods

Now that we understand how to build patterns, let's look at how to actually use them with JavaScript methods.

RegExp.prototype.test()

The test() method checks if a pattern exists in a string and returns true or false. It takes a string as its argument and belongs to the regex itself, not the string. Here's how it works:

const pattern = /\d+/;
pattern.test("16 inch pizza"); // true
pattern.test("Large pizza"); // false

This is straightforward when you're just checking for a match, but there's a gotcha you need to know about. If you use the g flag with test(), the regex object maintains state between calls. It keeps track of where it left off using an internal lastIndex property on the regex itself.

const pattern = /\d+/g;
pattern.test("Deliver to 123 Main St Apt 4, Sunnyvale"); // true
console.log(pattern.lastIndex); // 14
pattern.test("Deliver to 123 Main St Apt 4, Sunnyvale"); // true
console.log(pattern.lastIndex); // 28
pattern.test("Deliver to 123 Main St Apt 4, Sunnyvale"); // false
console.log(pattern.lastIndex); // 0 (wraps back to start)

Each call picks up where the previous one left off. This can cause unexpected results if you're testing the same regex against different strings or reusing a regex object. If you need to reset it, set lastIndex back to zero manually.

RegExp.prototype.exec()

The exec() method searches for a match and returns detailed information about it, including capture groups. It's called on a regex and takes a string as its argument. Like test(), it belongs to the regex object.

const pattern = /(pepperoni|mushrooms|sausage|olives|peppers)/;
const result = pattern.exec("I want mushrooms on my pizza");
console.log(result);
// ["mushrooms", "mushrooms", index: 7, input: "I want mushrooms on my pizza"]

When you have multiple capture groups, they're all included in the result array:

const pattern = /(large|medium|small)-(pepperoni|mushrooms|sausage)/;
const result = pattern.exec("Order: large-pepperoni");
console.log(result);
// ["large-pepperoni", "large", "pepperoni", index: 7, input: "Order: large-pepperoni"]

With named capture groups, the matched groups are accessible through the groups property:

const pattern = /\$(?<discount>\d+) off (?<size>large|medium|small)/;
const result = pattern.exec("Save $5 off large pizzas");
console.log(result.groups);
// { discount: "5", size: "large" }

Like test(), if you use the g flag with exec(), it maintains state and returns subsequent matches on repeated calls:

const pattern = /\d+/g;
pattern.exec("2 large 3 medium"); // ["2", index: 0, ...]
console.log(pattern.lastIndex); // 1
pattern.exec("2 large 3 medium"); // ["3", index: 8, ...]
console.log(pattern.lastIndex); // 9
pattern.exec("2 large 3 medium"); // null
console.log(pattern.lastIndex); // 0

String.prototype.match()

The match() method searches a string for a pattern and returns information about the matches. It's called on a string and takes a regex as its argument. Without the g flag, it returns an array with the first match, any capture groups, the index, and the input string.

const pattern = /(\d+)\s+(cups?|tsp|tbsp|oz)/;
const result = "Mix 2 cups flour with other ingredients".match(pattern);
console.log(result);
// ["2 cups", "2", "cups", index: 4, input: "Mix 2 cups flour with other ingredients", groups: undefined]

With the g flag, it returns an array of all matches but doesn't include capture groups or other details.

const pattern = /\d+/g;
const result = "Small $12, Medium $16, Large $20, XL $24".match(pattern);
console.log(result); // ["12", "16", "20", "24"]

If you need both global matching and capture groups, use matchAll() instead.

String.prototype.matchAll()

The matchAll() method returns a Regular Expression String Iterator of all matches with their capture groups. It requires the g flag on the regex. This is useful when you need detailed information about every match. To use array methods like .map(), .filter(), .forEach() directly, you can convert it to an array using Array.from() or [...] spread syntax.

const pattern = /(\w+):\s*(\d+)\s*oz/g;
const text = "Mozzarella: 8 oz, Parmesan: 2 oz, Provolone: 4 oz";
const matches = [...text.matchAll(pattern)];

matches.forEach((match) => {
  console.log(`Full match: ${match[0]}`);
  console.log(`Cheese: ${match[1]}`);
  console.log(`Amount: ${match[2]}`);
  console.log(`Index: ${match.index}`);
});
// Full match: Mozzarella: 8 oz
// Cheese: Mozzarella
// Amount: 8
// Index: 0
// Full match: Parmesan: 2 oz
// Cheese: Parmesan
// Amount: 2
// Index: 18
// Full match: Provolone: 4 oz
// Cheese: Provolone
// Amount: 4
// Index: 34

With named capture groups, you can access them through the groups property.

const pattern = /(?<cheese>\w+):\s*(?<amount>\d+)\s*oz/g;
const text = "Mozzarella: 8 oz, Parmesan: 2 oz, Provolone: 4 oz";
const matches = [...text.matchAll(pattern)];

matches.forEach((match) => {
  console.log(`${match.groups.cheese}: ${match.groups.amount}`);
});
// "Mozzarella: 8"
// "Parmesan: 2"
// "Provolone: 4"

String.prototype.search()

The search() method returns the index of the first match or -1 if there's no match. It's called on a string and takes a regex as its argument. The g flag doesn't affect this method since it only returns the first match's position.

const pattern = /stuffed/;
const result = "Hand-tossed with a stuffed crust edge".search(pattern);
console.log(result); // 23

This is useful when you just need to know where something is without caring about what was matched or any capture groups.

String.prototype.replace() and String.prototype.replaceAll()

The replace() method searches for a pattern and replaces it with a new string. It's called on a string and takes two arguments: the regex pattern and the replacement. Without the g flag, it only replaces the first match.

const pattern = /pepperoni/;
const result = "Add peppers, extra cheese, double pepperoni".replace(
  pattern,
  "sausage",
);
console.log(result); // "Add peppers, extra cheese, double sausage"

With the g flag, it replaces all matches.

const pattern = /medium/g;
const result = "Table 5 ordered medium, Table 8 wants medium too".replace(
  pattern,
  "large",
);
console.log(result); // "Table 5 ordered large, Table 8 wants large too"

You can reference capture groups in the replacement string using dollar signs. $1 refers to the first capture group, $2 to the second, and so on.

const pattern = /(\d+)\s+(pizzas?|salads?)/g;
const result = "Need 3 pizzas and 2 salads".replace(pattern, "$2: $1");
console.log(result); // "Need pizzas: 3 and salads: 2"

With named capture groups, you use $<name> syntax.

const pattern = /(?<quantity>\d+)\s+(?<item>pizzas?|salads?)/g;
const result = "Need 3 pizzas and 2 salads".replace(
  pattern,
  "$<item>: $<quantity>",
);
console.log(result); // "Need pizzas: 3 and salads: 2"

The second argument can also be a callback function that gets called for each match. The function receives the full match, any capture groups, the index, and the full string. Whatever you return becomes the replacement.

const pattern = /\d+/g;
const discount = 0.25; // 25% off
const result = "Pepperoni $18, Veggie $16, Supreme $22".replace(
  pattern,
  (match) => {
    return parseInt(match) * (1 - discount).toFixed(2);
  },
);
console.log(result); // "Pepperoni $13.50, Veggie $12.00, Supreme $16.50"

The replaceAll() method works the same way but requires the g flag if you're using a regex. It's there for consistency with the string version of replaceAll().

const pattern = /\[PENDING\]/g;
const result = "Order 1: [PENDING], Order 2: [PENDING]".replaceAll(
  pattern,
  "READY",
);
console.log(result); // "Order 1: READY, Order 2: READY"

String.prototype.split()

The split() method divides a string into an array based on a pattern. It's called on a string and takes a regex as its argument. This is where using regex as a delimiter really shines.

const pattern = /\s+/;
const result = "pepperoni    mushrooms   olives  peppers   sausage".split(
  pattern,
);
console.log(result); // ["pepperoni", "mushrooms", "olives", "peppers", "sausage"]

Without regex, you'd have to split on a single space and deal with the empty strings from multiple spaces. With regex, you can match any amount of whitespace at once.

You can also split on multiple different delimiters.

const pattern = /[\/\-]/;
const result = "large-pepperoni/medium-veggie/small-cheese".split(pattern);
console.log(result); // ["large", "pepperoni", "medium", "veggie", "small", "cheese"]

You can also use lookaheads and lookbehinds to split at specific positions without consuming any characters:

const pattern = /(?<=[a-z])(?=[A-Z])/;
const result = "largePepperonimediumVeggiesmallCheese".split(pattern);
console.log(result); // ["large", "Pepperoni", "medium", "Veggie", "small", "Cheese"]

Going Further

This guide covers the most commonly used regex features and methods in JavaScript, but there's a lot more to explore. If you want to dive deeper into regular expressions, the MDN documentation is an excellent resource with comprehensive coverage of all the features and edge cases.

Thanks For Stopping By!

I hope you've enjoyed my guide to Regular Expressions in Javascript. You now have what it takes to start leveraging their power. I am on Bsky @DevManSam if you'd like to connect or say hello.

Before we wrap up, here's something to decode:

const decodeThis =
  "7nks s$ mu2h f$r r#@d%ng! 8$p# &$u #nj$&#d l#@rn%ng @b$ut r#g#x @nd @ll th#s# p$w#rful m#th$ds. St@& 2h##s#&!";
console.log(
  decodeThis.replace(
    /[@#$%&2789]/g,
    (x) =>
      ({
        "@": "a",
        "#": "e",
        $: "o",
        "%": "i",
        "&": "y",
        2: "c",
        7: "Tha",
        8: "H",
        9: "",
      })[x],
  ),
);
// "Thanks so much for reading! Hope you enjoyed learning about regex and all these powerful methods. Stay cheesy!"