Regular Expressions in JavaScript
Regular expressions (regex) are patterns used to match character combinations in strings. In JavaScript, they’re a powerful tool for validation, search, and text manipulation. If you’ve ever needed to check if an email looks real, extract URLs from text, or find all the numbers in a string, regex is the answer.
Creating a regular expression
You can create a regex in two ways. The literal syntax is most common:
const pattern = /hello/;
Or you can use the RegExp constructor when the pattern comes from a variable:
const searchTerm = "hello";
const pattern = new RegExp(searchTerm);
Both produce a RegExp object. The literal syntax is cleaner for fixed patterns.
Basic Matching
At its simplest, a regex matches exact characters:
const regex = /hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("goodbye")); // false
The dot . matches any single character:
const regex = /c.t/;
console.log(regex.test("cat")); // true
console.log(regex.test("cut")); // true
console.log(regex.test("cart")); // false
Character Classes
Square brackets let you match specific sets of characters:
// Match any vowel
const vowelRegex = /[aeiou]/;
console.log(vowelRegex.test("bcd")); // false
console.log(vowelRegex.test("axe")); // true
// Match any digit
const digitRegex = /[0-9]/;
console.log(digitRegex.test("year2024")); // true
// Negate with ^
const notDigit = /[^0-9]/;
console.log(notDigit.test("abc")); // true
console.log(notDigit.test("123")); // false
Predefined character classes make common patterns shorter:
\w // Word character: [a-zA-Z0-9_]
\W // Non-word character
\d // Digit: [0-9]
\D // Non-digit
\s // Whitespace (space, tab, newline)
\S // Non-whitespace
. // Any character except newline
const hasDigit = /\d/;
const isWord = /\w+/;
console.log(hasDigit.test("hello")); // false
console.log(isWord.test("hello_123")); // true
Anchors
Anchors don’t match characters — they match positions in the string:
^ // Start of string
$ // End of string
\b // Word boundary
const startsWithNum = /^\d/;
console.log(startsWithNum.test("123abc")); // true
console.log(startsWithNum.test("abc123")); // false
const endsWithNum = /\d$/;
console.log(endsWithNum.test("abc123")); // true
console.log(endsWithNum.test("123abc")); // false
const wholeWord = /\bhello\b/;
console.log(wholeWord.test("hello world")); // true
console.log(wholeWord.test("helloworld")); // false
Quantifiers
Quantifiers specify how many times something should match:
* // 0 or more
+ // 1 or more
? // 0 or 1 (optional)
{n} // Exactly n times
{n,} // n or more times
{n,m} // Between n and m times
// Match one or more digits
const digits = /\d+/;
console.log(digits.test("a123b")); // true
console.log(digits.test("abc")); // false
// Match optional 's' at end
const plural = /dogs?/;
console.log(plural.test("dog")); // true
console.log(plural.test("dogs")); // true
// Match 3-5 letter word
const threeToFive = /\b\w{3,5}\b/;
console.log(threeToFive.test("hi")); // false
console.log(threeToFive.test("cat")); // true
console.log(threeToFive.test("elephant")); // false
Greedy vs lazy matching matters. By default, quantifiers are greedy — they match as much as possible:
const greedy = /".*"/; // Greedy
const lazy = /".*?"/; // Lazy (add ?)
const text = '"hello" "world"';
console.log(text.match(greedy)); // ['"hello" "world"']
console.log(text.match(lazy)); // ['"hello"', '"world"']
Groups and Backreferences
Parentheses create capturing groups, which let you extract parts of a match:
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = "2024-03-11".match(dateRegex);
console.log(match[0]); // "2024-03-11" (full match)
console.log(match[1]); // "2024" (first group)
console.log(match[2]); // "03" (second group)
console.log(match[3]); // "11" (third group)
You can reference groups later with \1, \2, etc:
// Match repeated words
const repeatedWord = /\b(\w+)\s\1\b/;
console.log(repeatedWord.test("the the")); // true
console.log(repeatedWord.test("the cat")); // false
Non-capturing groups use (?:...) when you need grouping but don’t want to capture:
const regex = /(?:Mr|Mrs|Ms)\. (\w+)/;
const match = "Mr. Smith".match(regex);
console.log(match[1]); // "Smith"
Flags
Flags modify how the regex behaves:
g // Global — find all matches, not just first
i // Case-insensitive
m // Multiline — ^ and $ match line starts/ends
s // Dotall — . matches newlines
u // Unicode — enables Unicode features
y // Sticky — match at lastIndex position only
// Case insensitive
const caseInsensitive = /hello/i;
console.log(caseInsensitive.test("HELLO")); // true
console.log(caseInsensitive.test("Hello")); // true
// Global - find all matches
const allDigits = /\d/g;
console.log("abc123def456".match(allDigits)); // ['1', '2', '3', '4', '5', '6']
// Multiline
const multiLine = /^line/m;
console.log("line1\nline2\nline".match(multiLine)); // ['line', 'line']
The u flag is essential for Unicode matching and enables Unicode property escapes:
const emojiRegex = /\p{Emoji}/u;
console.log(emojiRegex.test("😀")); // true
console.log(emojiRegex.test("a")); // false
Working with regex in JavaScript
RegExp Methods
The test() method returns true or false:
const hasUpper = /[A-Z]/;
console.log(hasUpper.test("hello")); // false
console.log(hasUpper.test("Hello")); // true
The exec() method returns match details or null:
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const result = regex.exec("Date: 2024-03-11");
if (result) {
console.log(result[0]); // "2024-03-11"
console.log(result.index); // 6
}
String Methods
The match() method returns an array of matches:
const str = "I have 3 cats and 5 dogs";
const numbers = str.match(/\d+/g);
console.log(numbers); // ['3', '5']
The replace() and replaceAll() methods let you substitute text:
// Replace first occurrence
const result1 = "hello world".replace(/o/, "x");
console.log(result1); // "hellx world"
// Replace all with global flag
const result2 = "hello world".replace(/o/g, "x");
console.log(result2); // "hellx wxrld"
// Use captured groups in replacement
const result3 = "2024-03-11".replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1");
console.log(result3); // "11/03/2024"
// Replacement function
const result4 = "abc123def".replace(/\d+/g, (match) => parseInt(match) * 2);
console.log(result4); // "abc246def"
The split() method uses regex as delimiter:
console.log("a,b;c:d".split(/[,;:/]/)); // ['a', 'b', 'c', 'd']
console.log("hello world".split(/\s+/)); // ['hello', 'world']
The search() method returns the index of the match:
console.log("hello world".search(/wor/)); // 6
console.log("hello world".search(/foo/)); // -1 (not found)
The matchAll() method returns an iterator of all matches with groups:
const regex = /(\d+)/g;
const str = "numbers: 1, 2, 3";
const matches = [...str.matchAll(regex)];
console.log(matches[0]); // ['1', '1', index: 9, ...]
console.log(matches[1]); // ['2', '2', index: 12, ...]
console.log(matches[2]); // ['3', '3', index: 15, ...]
Practical Examples
Email Validation
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
console.log(isValidEmail("test@example.com")); // true
console.log(isValidEmail("invalid@")); // false
console.log(isValidEmail("no@space here.com")); // false
Extracting URLs
const text = "Visit https://example.com or http://test.org for more info";
const urlRegex = /https?:\/\/[^\s]+/g;
const urls = text.match(urlRegex);
console.log(urls); // ['https://example.com', 'http://test.org']
Phone number formatting
function formatPhone(phone) {
const digits = phone.replace(/\D/g, "");
const match = digits.match(/^(\d{3})(\d{3})(\d{4})$/);
if (match) {
return `(${match[1]}) ${match[2]}-${match[3]}`;
}
return phone;
}
console.log(formatPhone("5551234567")); // "(555) 123-4567"
console.log(formatPhone("555-123-4567")); // "(555) 123-4567"
console.log(formatPhone("5551234")); // "5551234" (invalid)
Password Strength
function isStrongPassword(password) {
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasDigit = /\d/.test(password);
const hasSpecial = /[!@#$%^&*]/.test(password);
const longEnough = password.length >= 8;
const score = [hasUpper, hasLower, hasDigit, hasSpecial, longEnough]
.filter(Boolean).length;
return score >= 4;
}
console.log(isStrongPassword("abc123")); // false
console.log(isStrongPassword("Abc123!")); // true
console.log(isStrongPassword("SecureP@ss1")); // true
Common Pitfalls
Escaping special characters
Some characters have special meaning and need escaping:
// Match a literal dot
const dot = /\./;
console.log(dot.test("hello.world")); // true
// Match literal brackets
const brackets = /\[.*\]/;
console.log(brackets.test("[test]")); // true
Performance
Complex patterns can cause performance issues with catastrophic backtracking:
// Problematic: nested quantifiers can cause exponential backtracking
const badPattern = /(a+)+b/;
// Better: simpler pattern
const goodPattern = /a+b/;
Lookahead and Lookbehind
Lookaheads check what comes next without including it in the match:
// Positive lookahead: must be followed by "world"
const regex = /hello(?= world)/;
console.log(regex.test("hello world")); // true
console.log(regex.test("hello there")); // false
// Negative lookahead: must NOT be followed by "world"
const regex2 = /hello(?! world)/;
console.log(regex2.test("hello world")); // false
console.log(regex2.test("hello there")); // true
Lookbehind (ES2018+) checks what comes before:
// Positive lookbehind: must be preceded by "$"
const regex = /(?<=\$)\d+/;
console.log(regex.exec("price is $100")); // ["100"]
// Negative lookbehind: must NOT be preceded by "$"
const regex2 = /(?<!\$)\d+/;
console.log(regex2.test("$100")); // false
console.log(regex2.test("a100")); // true
Practical regex habits
Regular expressions are easier to use well when you keep them narrow. A pattern should solve one text problem instead of trying to validate every rule in a single line. That usually means splitting large tasks into smaller checks: trim the input, match the shape, then run a second rule if needed. This approach makes patterns easier to read and also easier to change when the format shifts. A small regex that is clear today is usually better than a giant one that nobody wants to touch next month.
It also helps to keep raw strings and regex literals distinct in your mind. A literal is great when the pattern is fixed, while new RegExp() is more useful when the pattern comes from a variable. When you build from user input, escape special characters first so the input is treated as text instead of syntax. That small habit prevents surprising matches and makes the code safer when the pattern is not fully under your control.
Debugging And Readability
When a regex fails, the easiest way to debug it is to simplify it until the match works again. Remove one piece at a time, test the smaller pattern, and then add the missing part back. That process is much easier when the expression is written with comments or split across steps in code. For example, you can test the structure first and then apply a transformation after the match is found. That keeps the problem space smaller and makes the code easier to explain in review.
Readable regex also depends on context. A pattern inside validation code should tell the reader what it is checking for, not just what symbols it uses. Naming the variable well helps, but so does a short comment that explains the intent when the pattern is not obvious. If a future maintainer has to pause and decode every part of the expression, the regex is probably doing too much. A slightly longer but clearer expression is often the better tradeoff.
Using regex with other tools
Regex becomes most useful when paired with other string and array operations. You can split text into lines, filter the results, then run a pattern on only the parts that matter. That makes the overall workflow easier to reason about than trying to capture everything in one expression. It also lets JavaScript handle the pieces that are not really regex problems, like sorting, counting, or converting types.
The same idea applies when you are extracting data. Use regex to identify the shape, then use normal JavaScript to map the output into a useful object. That separation keeps the pattern small and lets the surrounding code describe the business rule. In practice, that makes regex feel less like a magic trick and more like one focused tool among several.
A final regex habit
A good regex is usually the one that solves the exact text problem and no more. If the pattern starts getting hard to read, it is often better to split the job into two smaller checks than to keep piling on symbols. That approach makes the code easier to test and easier to update when the input format changes. It also helps future readers understand the intent without having to decode every special character.
When you are unsure about a pattern, write the simplest version that matches one real example and then grow from there. That is a safer path than trying to design a perfect expression in one pass. Small steps make regex feel less mysterious and much more practical in day-to-day code.
That habit also makes bugs easier to isolate because each piece of the expression has a clear job and a clear failure mode.
If a pattern is still hard to explain after you have simplified it, that is a sign that the logic belongs in a few smaller steps instead of one dense expression. Clearer steps make the code easier to maintain and easier to trust when the input changes.
That extra clarity usually pays off the first time the format shifts.
See Also
- JavaScript String Methods — Learn about string manipulation methods that work with regex
- JavaScript Closures Explained — Understand scope, which is essential for advanced regex patterns
- Asynchronous JavaScript — See how regex fits into async workflows