completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Regex performance & safety

Learn how JavaScript regex backtracking causes slow matches and ReDoS, then use sticky tokenizers, safer patterns, limits, and parsers.

By the end, you can
  • 01
    Trace backtrackingCount the tries in a tiny matcher, and spot when the count doubles with each extra letter.
  • 02
    Defend against ReDoSStack defenses for text that users send: safer patterns, length limits, workers, linters, and safer engines.
  • 03
    Tokenize and choose parsersUse the sticky y flag and lastIndex to build a tokenizer, and pick DOM, URL, JSON, date, or string tools when they fit better.

Why some regexes get very slow

A regular expression, or regex, describes a shape of text, like "one or more letters". JavaScript checks a regex with a backtracking engine. The engine tries one way to match. If that way fails, it goes back and tries another way.

Most patterns leave only a few ways to try. Some patterns leave a huge number of ways. Then one short string can keep the engine busy for a very long time before it can say "no match".

The main idea

Catastrophic backtracking means a regex has so many ways to try that the number of tries explodes as the text gets longer. ReDoS (Regular expression Denial of Service) is an attack that uses this. Someone sends text that makes your regex slow on purpose, so your page or server stops answering everyone else.

This is the last lesson in the Regular expressions module. It uses ideas from the earlier lessons: patterns and flags, character classes, greedy and lazy quantifiers, groups and backreferences, and anchors and lookaround. You don't need to fear regex. You only need to know when a pattern is safe to run on text that users send.

The table below lists three questions to ask about any regex. Read each row as: "ask this, aim for this, and watch out for this."

Three questions to ask about a regex
Question to askWhat to aim forWarning sign
Can the same letters be matched in many different ways?Write the pattern so there is only one way.A repeat inside a repeat, or two choices that can match the same letters.
Can a user type or send this text?Set a maximum length and check it first.The pattern runs on form fields, request bodies, search boxes, or log lines.
Is the text a real format, like HTML or JSON?Use a built-in parser instead.HTML, URLs, JSON, dates, or full email checks.

In this lesson you will:

  • step through a tiny matcher and count its tries,
  • watch the count grow for two risky patterns,
  • build a small tokenizer with the sticky flag,
  • choose built-in tools instead of a regex: URL objects, JSON.parse, the DOM tree, and string methods.

How the engine goes back and tries again

STEP THROUGH

Backtracking means going back to the last choice and trying a different one. A quantifier like + makes a choice: how many letters to take. A greedy quantifier first takes as many letters as it can. If a later part of the pattern then fails, the engine goes back. It makes the quantifier give one letter back, and it tries again.

Real-life analogyA maze with many doors

Think of each choice as a door in a maze. You pick a door and walk on. If the exit is locked, you walk back to the last door and pick another one. If every door leads to more doors, even a small maze takes a long time to search.

In real life: Walk down the longest hallway first
In JavaScript: A greedy a+ takes every a it can
In real life: The exit at the end is locked
In JavaScript: The $ fails, because the next character is !
In real life: Walk back and try a shorter hallway
In JavaScript: The engine backtracks and tries a smaller stop

The experiment below is a tiny matcher written in plain JavaScript. It handles only one pattern, (a+)+$, checked from the start of the text:

  • a+ means one or more a letters. Call that a chunk.
  • The round brackets and the outer + repeat the chunk one or more times.
  • $ means "the text must end here".

The matcher counts every step it takes. At the end it checks that its answer, true or false, is the same as the real JavaScript regex answer.

Step through the tiny matcher
Step 0 of 11Ready
Your turn: follow the blue line

Step through a tiny matcher for the pattern (a+)+$. It counts steps, so you can watch the work grow without timing the real engine.

Running in
  1. script
Next: line 18
function matchNested(input) {  let steps = 0;  function repeatFrom(index) {    steps++;    if (index === input.length) return index > 0;    if (input[index] !== "a") return false;    let end = index;    while (input[end] === "a") end++;    for (let stop = end; stop > index; stop--) {      if (repeatFrom(stop)) return true;    }    return false;  }  const matched = repeatFrom(0);  return { matched, steps };} const result = matchNested(input);console.log(result.matched);console.log(result.steps);console.log(/^(a+)+$/.test(input));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Look at the loop over stop on line 9. It decides how many letters the inner a+ takes. For "aaa!", it can cut the three letters into chunks in four ways:

  • aaa as one chunk,
  • aa, then a,
  • a, then aa,
  • a, a, and a.

Every way fails, because after the letters comes !, not the end of the text.

The pattern (a+)+$ on the text aaa!. Try 1 cuts aaa into one chunk. Try 2 cuts it into aa and a. Try 3 cuts it into a and aa. Try 4 cuts it into a, a, and a. Every try then reaches the ! and the $ fails there.(a+)+$ on "aaa!"try 1aaa!$ fails at !try 2aaa!$ fails at !try 3aaa!$ fails at !try 4aaa!$ fails at !
The inner a+ can cut aaa into chunks in four ways. Every way reaches the !, so the match fails only after all four tries.

The next lab counts the steps for longer texts. It also counts a second risky pattern, (a|aa)+$, which means "one or more pieces, where each piece is a or aa".

Count the steps as the failing text grows
The tiny matcher the lab usesPop out in the code editor (opens in a new tab)JavaScript
function matchNested(input) {  let steps = 0;  function repeatFrom(index) {    steps++;    if (index === input.length) return index > 0;    if (input[index] !== "a") return false;    let end = index;    while (input[end] === "a") end++;    for (let stop = end; stop > index; stop--) {      if (repeatFrom(stop)) return true;    }    return false;  }  const matched = repeatFrom(0);  return { matched, steps };} const input = "aaa!";const result = matchNested(input);console.log(result.matched);console.log(result.steps);console.log(/^(a+)+$/.test(input));
Steps countedaaaaa!
Pattern(a+)+$32real regex says: false
Pattern(a|aa)+$20real regex says: false
letters(a+)+$(a|aa)+$
122
244
387
41612
53220
66433
712854
Try it yourself

The counts come from the lesson's tiny matcher, not from timing the browser's regex. The real regex is used only to check that both give the same true or false.

Move the slider all the way up. The text stays short, so the page stays fast.

For (a+)+$, each extra letter doubles the count. The second pattern grows a little slower, but its count still climbs very fast.

Steps counted for (a+)+$. The input aaa! takes 8 steps. The input aaaaa! takes 32 steps. The input aaaaaaa! takes 128 steps.failing inputsteps for (a+)+$aaa!8 stepsaaaaa!32 stepsaaaaaaa!128 steps
Each extra a doubles the steps for (a+)+$. Two more letters means four times the work.
We count steps instead of timing

This page never times a slow real regex, because that could freeze your browser. The slider only counts steps in the tiny matcher, and the texts stay short. The lesson's tests check that the tiny matcher gives the same true or false as JavaScript for these short texts.

Three pattern shapes that cause too many tries

SORT IT

When you check a regex for speed, first name its shape. A long pattern can be safe. A short pattern can be dangerous. The danger comes from ambiguity: the same letters can be matched in more than one way, and the number of ways grows with the text.

There are three shapes to look for:

  • Nested quantifier: a repeat inside a repeat, like (a+)+.
  • Overlapping choices: two choices that can match the same letters, like (a|aa)+.
  • Side-by-side repeats: two repeats next to each other that both want the same letters, like a*a*.
Three risky shapes to name first
The three risky shapesPop out in the code editor (opens in a new tab)JavaScript
const examples = [  "nested quantifier: ^(a+)+$",  "overlapping choices: ^(a|aa)+$",  "side-by-side repeats: ^a*a*$",];console.log(examples.length);console.log(examples[0].includes("nested"));
Try it yourself
Read before sorting

Once you can name the shape, the fix is easier: remove the repeat inside a repeat, stop the choices from overlapping, or put a real separator between side-by-side repeats.

The sorter below lets you practice naming these shapes.
Name the risky shape
  • ^(a+)+$
  • ^(.*)+!$
  • ^(a|aa)+$
  • ^(tea|teatea)+!$
  • ^a*a*$
  • ^\w+\w+$
  • /<div>.*<\/div>/ for real HTML
  • /{.*}/ for JSON
Try it yourself
0 of 8 correct

Sort each pattern by the shape that makes it risky. Some cards are not regex jobs at all.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The table below pairs each risky shape with a safer way to write it. Read each row as: "this shape is risky, so do this instead."

Risky shapes and safer ways to write them
The problemRisky exampleWhat to do instead
Nested quantifier^(a+)+$Use ^[a]+$, or a loop that checks each character once.
Overlapping choices^(a|aa)+$Make sure the choices can't match the same letters, or read one token at a time.
Side-by-side repeats^a*a*$Use one repeat, add a real separator, or use character classes that can't take the same letters.
Text of any lengthAny complex patternReject text that is too long before matching. Don't cut it short, because that changes what it says.

Anchors like ^ and $ only say where a match must start or end. They don't remove the choices inside the pattern. If the rule is simply "one or more a letters", remove the repeated group.

Remove the repeat inside a repeatPop out in the code editor (opens in a new tab)JavaScript
const input = "aaaa!";
console.log(/^(a+)+$/.test(input));
console.log(/^a+(?![\s\S])/.test(input));
console.log(/^a+(?![\s\S])/.test("aaaa"));
console.log(/^a+(?![\s\S])/.test("aaaa\n"));

Line by line:

  1. Line 1 stores the text "aaaa!".
  2. Line 2 prints false. Before it gives up, the nested group tries many ways to cut the letters into chunks.
  3. Line 3 also prints false, with far fewer tries. There is only one repeat, so it tries at most one way per letter.
  4. Line 4 prints true. The new pattern still accepts aaaa.
  5. Line 5 prints false for aaaa with a line break at the end.

The end check (?![\s\S]) is a lookahead. It means "no character of any kind comes next", so it passes only at the very end of the text. Unlike $, it keeps working that way even if someone later adds the m flag. These lines check results only. They don't time the slow pattern.

Some other regex languages have tools that stop backtracking: possessive quantifiers, like a++, and atomic groups, like (?>...). JavaScript has neither. So in JavaScript, keep patterns safe in these ways:

  • Use one clear character class instead of a repeat inside a repeat.
  • Make sure two choices can't match the same letters.
  • Check the whole text from start to end when you mean the whole text.
  • Limit the length of the text before you match.

The panel below uses these rules to check a slug: the short lowercase name at the end of a web address, like regex-performance.

A safer pattern: a slug with a length limit
Check the length, then the shapePop out in the code editor (opens in a new tab)JavaScript
function acceptsSlug(slug) {  if (slug.length > 32) return false;  return /^[a-z]+(?:-[a-z]+)*(?![\s\S])/.test(slug);} console.log(acceptsSlug("regex-performance"));console.log(acceptsSlug("regex--performance"));console.log(acceptsSlug("a".repeat(40)));console.log(acceptsSlug("regex-performance\n"));
Outputreal results
call 1true
call 2false
call 3false
call 4false
Try it yourself
Length check before regex

The pattern has one clear separator, a check for the real end of the text, and a length limit that runs first. The fourth call rejects a line break at the end.

This code checks the shape of a slug. It isn't a full parser.

ReDoS: one bad string blocks everyone

PRODUCTION

In the browser, JavaScript runs your code on one main thread: a single worker that does one job at a time. Node runs a server's code the same way, with an event loop that takes jobs one by one. A regex match is a job that keeps the computer busy. While it runs, nothing else runs: no clicks, no timers, no page updates, and no other server requests.

ReDoS is not only a browser problem

On a server, a slow regex can run on a request body, a header, a username, or a log line. While the server is busy with that one match, every other request waits. That is why regex speed is a security problem, not just a speed problem.

A Node server window is busy running one slow regex, (a+)+$. Three more requests wait outside in a line. They can't start until the match ends.request 2request 3request 4all waitNode serverregex: (a+)+$busy with one slow match
The server runs your JavaScript on one thread. While one slow match runs, every other request waits in line.

This has happened to large websites:

  • Stack Overflow, July 2016. A post with about 20,000 space characters in a row made a regex that removes trailing spaces backtrack a huge amount. The site was down for 34 minutes. The report gave the simplified pattern as \s+$. Its work grew with the square of the length (quadratic), not by doubling (exponential). That was still enough to take the site down. Source: ADTmag’s summary of the StackStatus postmortem.
  • Cloudflare, July 2019. Cloudflare added a new rule to its WAF (Web Application Firewall), a filter that blocks bad web traffic. The rule had a regex that pushed CPU use to 100% on the machines that serve web pages. Cloudflare was down worldwide for 27 minutes, from 13:42 to 14:09 UTC, and traffic dropped by 82% at the worst point. Source: Cloudflare’s postmortem.

You can stack several defenses. Read each row of the table as: "this defense gives you this, and this is how you use it."

Defenses you can stack
DefenseWhat it gives youHow to use it
Length limitA quick first checkReject text that is too long before the regex runs. Write the limit down.
Rewrite risky shapesThe fix that lastsRemove nested quantifiers, overlapping choices, and side-by-side repeats.
Worker with a timerKeeps the page responsiveRun untrusted patterns in a Web Worker, and call terminate() if it takes too long.
Linters and scannersWarnings before you shipUse eslint-plugin-regexp and tools like safe-regex. They can miss problems or warn about safe patterns.
RE2A speed promiseIts Node bindings never backtrack, so time grows only with text length. They can't run backreferences or lookaround.

For a short profile tag, check the length first and reject text that is too long. Don't cut the text down to size instead. Cutting could turn a bad tag into a different tag that passes.

Check a profile tag's length before matchingPop out in the code editor (opens in a new tab)JavaScript
function acceptsTag(tag) {
  if (tag.length > 12) return false;
  return /^[a-z]+(?![\s\S])/.test(tag);
}
console.log(acceptsTag("tea"));
console.log(acceptsTag("a".repeat(13)));
console.log(acceptsTag("tea!"));

Line by line:

  1. Line 2 returns false for any tag longer than 12, before the regex runs.
  2. Line 3 accepts only lowercase letters, all the way to the very end of the text.
  3. Lines 5 to 7 print true, then false (13 letters is too long), then false (! is not a letter).

length counts JavaScript string units, not the characters you see, so this rule is meant for plain English letters only. A length limit keeps the work small, but it doesn't fix a risky pattern by itself.

In the browser, you can run an untrusted pattern in a Web Worker: a separate thread that runs beside the page. A timer can't stop a regex that is already running on the main thread. It can stop a worker, with terminate().

The code below is only the page's half. It also needs a separate file, regex-worker.js, that runs the match and sends back the result. So it won't run by itself in the lesson editor.

Stop a slow worker with a timerJavaScript
const worker = new Worker("regex-worker.js");const timeout = setTimeout(() => {  worker.terminate();  console.log("regex worker stopped");}, 50); worker.onmessage = (event) => {  clearTimeout(timeout);  console.log(event.data.matched);}; worker.postMessage({ pattern: "^(a+)+$", input: "aaaa!" });
  1. Line 1 starts the worker.
  2. Lines 2 to 5 start a 50-millisecond timer. If the timer fires first, line 3 stops the worker and line 4 prints a note.
  3. Lines 7 to 10 handle the worker's answer. Line 8 cancels the timer, and line 9 prints the result.
  4. Line 12 sends the pattern and the text to the worker.

Tools can also catch risky patterns before your code runs:

  • eslint-plugin-regexp finds regex mistakes and suggests clearer or faster patterns.
  • safe-regex warns about many patterns that can get very slow. Its own docs say it can miss some risky patterns and can warn about safe ones.
  • For Node servers, RE2 bindings use an engine that never backtracks, so its time grows only in step with the text length. It can't run backreferences or lookaround.
  • V8, the engine inside Chrome and Node, has an experimental engine that doesn't backtrack. It turns on only with special flags, so don't count on it in ordinary JavaScript.

The sticky flag and lastIndex

STEP THROUGH

A regex with the g (global) flag or the y (sticky) flag remembers a position in a property called lastIndex. The next match starts from there. The two flags differ in one way:

  • With g, the search starts at lastIndex and may move ahead to a later match.
  • With y, the match must start exactly at lastIndex, or it fails.

Read each row of the table as: "in this situation, this is what g does, and this is what y does."

g versus y
Situationg (global)y (sticky)
Where the match may startAt lastIndex, or anywhere after itOnly exactly at lastIndex
After a matchlastIndex moves to just after the matchlastIndex moves to just after the match
After a failed matchlastIndex goes back to 0lastIndex goes back to 0
Best forFinding every match in a textTokenizers, which must not skip any characters

That exact position is just right for a tokenizer, also called a lexer. A tokenizer is code that cuts text into small pieces called tokens, like words, numbers, and signs.

The tokenizer below skips any spaces, then reads one name, one number, or one = sign. If the next character can't start a token, it stops right there with an error. It never jumps ahead and hides the problem.

Sticky tokenizer with lastIndex
Step 0 of 17Ready
Your turn: follow the blue line

Step through a tokenizer that uses the sticky y flag. Watch lastIndex become the place where every token must start.

Running in
  1. script
Next: line 1
const input = "let count = 3";const parts = []; while (token.lastIndex < input.length) {  const before = token.lastIndex;  const match = token.exec(input);  if (!match) throw new Error("stuck at " + before);  parts.push(match[1]);} console.log(parts.join(" | "));console.log(token.lastIndex);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The next panel runs the same search on "xxcat" with both flags. Both start at lastIndex 1.

g moves ahead; y does not
Global and sticky, both from index 1Pop out in the code editor (opens in a new tab)JavaScript
const text = "xxcat";const global = /cat/g;global.lastIndex = 1;const foundGlobal = global.exec(text);console.log(foundGlobal.index);console.log(global.lastIndex); const sticky = /cat/y;sticky.lastIndex = 1;console.log(sticky.exec(text));console.log(sticky.lastIndex);
Outputreal values
global match index2
global lastIndex5
sticky exec resultnull
sticky lastIndex0
Try it yourself
Same text, two flags

The global regex starts at index 1 and finds cat at index 2. The sticky regex starts at index 1, won't skip the x, returns null, and sets lastIndex back to 0.

Both flags use lastIndex. Sticky makes the match start exactly there.
The text xxcat, with lastIndex set to 1. Left: /cat/g starts at 1, moves past the x, and finds cat at index 2, so lastIndex becomes 5. Right: /cat/y must match at index 1. The x there is not c, so exec gives null and lastIndex goes back to 0./cat/g012xxcatstarts at 1, moves onfinds cat at 2lastIndex = 5/cat/y012xxcatmust match at 1x is not c: nulllastIndex = 0
Both start at lastIndex 1. With g, the search may move ahead to find cat. With y, the match must start right at index 1, so it fails.

A regex with g or y keeps its lastIndex between calls. So if you use one regex for several separate form fields, the next field may start in the wrong place, even when it holds the same text.

Reset a reused sticky regexPop out in the code editor (opens in a new tab)JavaScript
const digit = /\d/y;
console.log(digit.test("3"));
digit.lastIndex = 0;
console.log(digit.test("3"));
console.log(digit.test("3"));
  1. Line 1 makes a sticky regex that matches one digit.
  2. Line 2 matches "3" and prints true. lastIndex is now 1.
  3. Line 3 sets lastIndex back to 0 before the next field.
  4. Line 4 prints true again.
  5. Line 5 has no reset. It starts at index 1, finds no digit there, and prints false.

So the output is true, true, false. Reset lastIndex before each separate text, or use a regex without g or y.

A failed match resets lastIndex

When a match with g or y fails, exec and test set lastIndex back to 0. So if you want an error like "unexpected character at index 5", save lastIndex before you call exec. This tokenizer also fails on spaces at the end of the text, because after it skips spaces, its pattern needs a token.

When not to use a regex

CHOOSE API

A regex is good for simple text shapes: a slug, a short token, a known start, or one small piece of a line. It is the wrong first tool when JavaScript already has a parser for the format. A parser is code that reads a format by all of its rules.

Read each row of the table as: "for this job, a regex can go wrong like this, so use this instead."

A regex or a built-in parser?
JobWhat goes wrong with a regexWhat to use instead
HTMLTags inside tags and broken markup confuse text patterns.DOMParser or DOM methods
URLsIt's easy to miss special characters, base addresses, and encoded queries.new URL() and URLSearchParams
JSONA regex can't reliably follow strings inside objects and escape characters.JSON.parse
EmailThe only real test is whether an email arrives.A simple shape check, then a confirmation email
DatesCalendars, languages, and time zones have many rules.Temporal where it exists, and Intl to show dates
Simple textA regex is often harder to read than a method call.includes, startsWith, endsWith, split
Use DOMParser for HTMLPop out in the code editor (opens in a new tab)JavaScript
const doc = new DOMParser().parseFromString(  "<article><h1>Regex</h1><p>Use a parser.</p></article>",  "text/html",);console.log(doc.querySelector("h1")?.textContent);console.log(doc.querySelector("p")?.textContent);

Line 1 asks DOMParser to read the HTML string and build a document from it. Lines 5 and 6 find the h1 and p elements and print their text: Regex and Use a parser.

Use built-in tools for other formatsPop out in the code editor (opens in a new tab)JavaScript
const page = new URL("/search?q=regex", "https://example.com");console.log(page.searchParams.get("q"));console.log(JSON.parse('{"safe":true}').safe);console.log("lesson.regex.md".endsWith(".md"));console.log(new Intl.DateTimeFormat("en-US", {  month: "short",  timeZone: "UTC",}).format(new Date("2026-09-26T00:00:00Z")));
  1. Lines 1 and 2: URL reads the address, and searchParams.get("q") prints regex.
  2. Line 3: JSON.parse reads the JSON and prints true.
  3. Line 4: endsWith checks the end of the file name and prints true.
  4. Lines 5 to 8: Intl.DateTimeFormat prints the short month name, Sep.

For a simple check on a message, string methods say what you mean with no regex at all. They compare letters exactly, so uppercase and lowercase count as different letters.

Check a notification with string methodsPop out in the code editor (opens in a new tab)JavaScript
const message = "Order ready: tea";
console.log(message.startsWith("Order ready:"));
console.log(message.includes("tea"));
console.log(message.includes("Tea"));
  1. Line 2 prints true: the message starts with Order ready:.
  2. Line 3 prints true: the message contains tea.
  3. Line 4 prints false: uppercase T is not the same letter as lowercase t.

If a search should ignore uppercase and lowercase, change both strings to lowercase first, for example with toLowerCase().

If you check many values with the same regex, make the regex once, outside the loop. Leave off g and y, so each value is checked from the start.

Make one regex and reuse itPop out in the code editor (opens in a new tab)JavaScript
const tagPattern = /^[a-z]+(?![\s\S])/;
for (const tag of ["tea", "tea!"]) {
  console.log(tagPattern.test(tag));
}

Line 1 makes the whole-tag check once. Line 2 loops over two tags. Line 3 prints true for tea and false for tea!. No lastIndex carries over between the calls. Making the regex once saves building it again for every item, but it doesn't make a risky pattern safe.

Be extra careful with email. A giant "perfect email regex" is hard to read and still gets real addresses wrong. For most sites, keep the check in the browser simple. Then send an email to prove the address works.

Dates are similar. Use Temporal for date math where it exists, and Intl to show dates. Don't try to fit calendars and time zones into a pattern.

Common misconceptions

  • "Lazy quantifiers are safe." Lazy quantifiers, like +?, still backtrack. They just try the shortest choice first.
  • "Anchors fix slow patterns." Anchors limit where a match can start or end. They don't remove the extra choices inside the pattern.
  • "If it's fast on my test strings, it's safe." Risky patterns often look fine on normal text. Then someone sends text that fails near the end, adds a few more letters, and the time explodes.
  • "A tool said my regex is safe, so it is." Linters and scanners give useful warnings. But they can miss a risky pattern or warn about a safe one. Treat them as one layer of defense.
  • "A regex is always faster than string methods." To check "contains", "starts with", or "ends with", or to split text, string methods are clearer. Which one is faster depends on the text and the engine, so measure only when speed really matters.

Read each row of the table as: "people believe this, so do this instead, for this reason."

Beliefs to replace
What people believeWhat to do insteadWhy
.* is harmless when the text is short.Ask who controls the text, and limit its length.Attackers send unusual text on purpose, not your demo text.
A clever regex can replace a parser.Use the parser for real formats.HTML, JSON, URLs, and dates already have full rules and tools.
A worker makes the regex safe.A worker only keeps the damage away from the page.You still need a timeout and a careful look at the pattern.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the tiny matcher's count

Read the tiny matcher and type the number it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function matchNested(input) {
  let steps = 0;
  function repeatFrom(index) {
    steps++;
    if (index === input.length) return true;
    if (input[index] !== "a") return false;
    let end = index;
    while (input[end] === "a") end++;
    for (let stop = end; stop > index; stop--) {
      if (repeatFrom(stop)) return true;
    }
    return false;
  }
  const matched = repeatFrom(0);
  return { matched, steps };
}
const result = matchNested("aaa!");
console.log(result.steps);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeRead the sticky tokenizer's output

    Predict the exact text the sticky tokenizer prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const token = /\s*([A-Za-z_]\w*|\d+|=)/y;
    const input = "let count = 3";
    const parts = [];
    while (token.lastIndex < input.length) {
      const match = token.exec(input);
      parts.push(match[1]);
    }
    console.log(parts.join(" | "));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeA failed sticky match

      After the failed match, what number does the code print for lastIndex?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const sticky = /cat/y;
      sticky.lastIndex = 1;
      console.log(sticky.exec("xxcat"));
      console.log(sticky.lastIndex);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeCheck the safe slug pattern

        Type the two booleans the slug checker prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function acceptsSlug(slug) {
          if (slug.length > 32) return false;
          return /^[a-z]+(?:-[a-z]+)*(?![\s\S])/.test(slug);
        }
        console.log(acceptsSlug("regex-performance"));
        console.log(acceptsSlug("regex--performance"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose the parser

          Which built-in object should you use to read URLs instead of a regex?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const page = new URL("/search?q=regex", "https://example.com");
          console.log(page.searchParams.get("q"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is catastrophic backtracking?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the tiny matcher print for aaa!?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function matchNested(input) {
                let steps = 0;
                function repeatFrom(index) {
                  steps++;
                  if (index === input.length) return true;
                  if (input[index] !== "a") return false;
                  let end = index;
                  while (input[end] === "a") end++;
                  for (let stop = end; stop > index; stop--) {
                    if (repeatFrom(stop)) return true;
                  }
                  return false;
                }
                const matched = repeatFrom(0);
                return { matched, steps };
              }
              const result = matchNested("aaa!");
              console.log(result.steps);

              Choose an answer to see the explanation.

            3. Question 3 of 7Which risky shape does ^(a|aa)+$ contain?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this sticky miss leave in lastIndex?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const sticky = /cat/y;
              sticky.lastIndex = 1;
              console.log(sticky.exec("xxcat"));
              console.log(sticky.lastIndex);

              Choose an answer to see the explanation.

            5. Question 5 of 7Why is ReDoS especially dangerous in Node.js servers?

              Choose an answer to see the explanation.

            6. Question 6 of 7Which statement about RE2 is accurate?

              Choose an answer to see the explanation.

            7. Question 7 of 7JSON.parse reads this JSON string. What does the code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const text = '{"ok":true}';
              console.log(JSON.parse(text).ok);

              Choose an answer to see the explanation.

            Key takeaways

            • A backtracking engine can try many equal ways to match before it finally fails.
            • Watch for three risky shapes: nested quantifiers, overlapping choices, and side-by-side repeats.
            • ReDoS blocks real work, because a regex match keeps the browser's main thread or Node's event loop busy.
            • The sticky y flag makes lastIndex the exact place where the next token must start. A failed match with g or y sets it back to 0.
            • Use parsers and built-in tools for HTML, URLs, JSON, dates, email checks, and simple text checks.

            Remember the one-liner.
            A safe regex leaves few choices, runs only on text of limited length, and steps aside for a parser when the text has real rules.

            That ends the Regular expressions module. Next comes typed arrays, the first lesson in Binary data & memory. It moves from text patterns to bytes, buffers, and views into memory.

            CompleteFrontend Clear concepts. Working examples.