Regex performance & safety
Learn how JavaScript regex backtracking causes slow matches and ReDoS, then use sticky tokenizers, safer patterns, limits, and parsers.
- 01Trace backtrackingCount the tries in a tiny matcher, and spot when the count doubles with each extra letter.
- 02Defend against ReDoSStack defenses for text that users send: safer patterns, length limits, workers, linters, and safer engines.
- 03Tokenize and choose parsersUse the sticky
yflag andlastIndexto 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".
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."
| Question to ask | What to aim for | Warning 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 THROUGHBacktracking 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.
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 everyait 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 morealetters. 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 a tiny matcher for the pattern (a+)+$. It counts steps, so you can watch the work grow without timing the real engine.
script
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));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:
aaaas one chunk,aa, thena,a, thenaa,a,a, anda.
Every way fails, because after the letters comes !, not the end of the text.
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".
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));(a+)+$32real regex says: false(a|aa)+$20real regex says: false| letters | (a+)+$ | (a|aa)+$ |
|---|---|---|
| 1 | 2 | 2 |
| 2 | 4 | 4 |
| 3 | 8 | 7 |
| 4 | 16 | 12 |
| 5 | 32 | 20 |
| 6 | 64 | 33 |
| 7 | 128 | 54 |
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.
For (a+)+$, each extra letter doubles the count. The second pattern grows a little slower, but its count still climbs very fast.
a doubles the steps for (a+)+$. Two more letters means four times the work.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 ITWhen 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*.
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"));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.
^(a+)+$^(.*)+!$^(a|aa)+$^(tea|teatea)+!$^a*a*$^\w+\w+$/<div>.*<\/div>/for real HTML/{.*}/for JSON
Sort each pattern by the shape that makes it risky. Some cards are not regex jobs at 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."
| The problem | Risky example | What 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 length | Any complex pattern | Reject 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.
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:
- Line 1 stores the text
"aaaa!". - Line 2 prints
false. Before it gives up, the nested group tries many ways to cut the letters into chunks. - Line 3 also prints
false, with far fewer tries. There is only one repeat, so it tries at most one way per letter. - Line 4 prints
true. The new pattern still acceptsaaaa. - Line 5 prints
falseforaaaawith 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.
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"));truefalsefalsefalseThe 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.
ReDoS: one bad string blocks everyone
PRODUCTIONIn 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.
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.
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."
| Defense | What it gives you | How to use it |
|---|---|---|
| Length limit | A quick first check | Reject text that is too long before the regex runs. Write the limit down. |
| Rewrite risky shapes | The fix that lasts | Remove nested quantifiers, overlapping choices, and side-by-side repeats. |
| Worker with a timer | Keeps the page responsive | Run untrusted patterns in a Web Worker, and call terminate() if it takes too long. |
| Linters and scanners | Warnings before you ship | Use eslint-plugin-regexp and tools like safe-regex. They can miss problems or warn about safe patterns. |
| RE2 | A speed promise | Its 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.
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:
- Line 2 returns
falsefor any tag longer than 12, before the regex runs. - Line 3 accepts only lowercase letters, all the way to the very end of the text.
- Lines 5 to 7 print
true, thenfalse(13 letters is too long), thenfalse(!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.
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!" });- Line 1 starts the worker.
- 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.
- Lines 7 to 10 handle the worker's answer. Line 8 cancels the timer, and line 9 prints the result.
- 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 THROUGHA 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 atlastIndexand may move ahead to a later match. - With
y, the match must start exactly atlastIndex, or it fails.
Read each row of the table as: "in this situation, this is what g does, and this is what y does."
| Situation | g (global) | y (sticky) |
|---|---|---|
| Where the match may start | At lastIndex, or anywhere after it | Only exactly at lastIndex |
| After a match | lastIndex moves to just after the match | lastIndex moves to just after the match |
| After a failed match | lastIndex goes back to 0 | lastIndex goes back to 0 |
| Best for | Finding every match in a text | Tokenizers, 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.
Step through a tokenizer that uses the sticky y flag. Watch lastIndex become the place where every token must start.
script
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);The next panel runs the same search on "xxcat" with both flags. Both start at lastIndex 1.
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);25null0The 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.
lastIndex. Sticky makes the match start exactly there.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.
const digit = /\d/y;
console.log(digit.test("3"));
digit.lastIndex = 0;
console.log(digit.test("3"));
console.log(digit.test("3"));- Line 1 makes a sticky regex that matches one digit.
- Line 2 matches
"3"and printstrue.lastIndexis now 1. - Line 3 sets
lastIndexback to 0 before the next field. - Line 4 prints
trueagain. - 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.
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 APIA 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."
| Job | What goes wrong with a regex | What to use instead |
|---|---|---|
| HTML | Tags inside tags and broken markup confuse text patterns. | DOMParser or DOM methods |
| URLs | It's easy to miss special characters, base addresses, and encoded queries. | new URL() and URLSearchParams |
| JSON | A regex can't reliably follow strings inside objects and escape characters. | JSON.parse |
| The only real test is whether an email arrives. | A simple shape check, then a confirmation email | |
| Dates | Calendars, languages, and time zones have many rules. | Temporal where it exists, and Intl to show dates |
| Simple text | A regex is often harder to read than a method call. | includes, startsWith, endsWith, split |
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.
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")));- Lines 1 and 2:
URLreads the address, andsearchParams.get("q")printsregex. - Line 3:
JSON.parsereads the JSON and printstrue. - Line 4:
endsWithchecks the end of the file name and printstrue. - Lines 5 to 8:
Intl.DateTimeFormatprints 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.
const message = "Order ready: tea";
console.log(message.startsWith("Order ready:"));
console.log(message.includes("tea"));
console.log(message.includes("Tea"));- Line 2 prints
true: the message starts withOrder ready:. - Line 3 prints
true: the message containstea. - Line 4 prints
false: uppercaseTis not the same letter as lowercaset.
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.
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."
| What people believe | What to do instead | Why |
|---|---|---|
.* 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 EXERCISESRead the tiny matcher and type the number it prints.
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);The tiny matcher makes 8 calls to repeatFrom before every way of cutting aaa fails at the !. So it prints 8.
Predict the exact text the sticky tokenizer prints.
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(" | "));The sticky regex reads from left to right and captures let, count, =, and 3. Joined with | , that prints let | count | = | 3.
After the failed match, what number does the code print for lastIndex?
const sticky = /cat/y;
sticky.lastIndex = 1;
console.log(sticky.exec("xxcat"));
console.log(sticky.lastIndex);/cat/y can't match at index 1, so exec returns null and sets lastIndex back to 0.
Type the two booleans the slug checker prints.
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"));regex-performance passes. regex--performance fails, because the group (?:-[a-z]+) needs letters after each hyphen, not another hyphen.
Which built-in object should you use to read URLs instead of a regex?
const page = new URL("/search?q=regex", "https://example.com");
console.log(page.searchParams.get("q"));const page = new URL('/search?q=regex', 'https://example.com');Use a URL object and its searchParams to read URLs, not a hand-written regex.
Check your understanding
7 QUESTIONSQuestion 1 of 7What is catastrophic backtracking?
Choose an answer to see the explanation.
Question 2 of 7What does the tiny matcher print for
aaa!?Read the code, then predictfunction 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.
Question 3 of 7Which risky shape does
^(a|aa)+$contain?Choose an answer to see the explanation.
Question 4 of 7What does this sticky miss leave in
lastIndex?Read the code, then predictconst sticky = /cat/y; sticky.lastIndex = 1; console.log(sticky.exec("xxcat")); console.log(sticky.lastIndex);Choose an answer to see the explanation.
Question 5 of 7Why is ReDoS especially dangerous in Node.js servers?
Choose an answer to see the explanation.
Question 6 of 7Which statement about RE2 is accurate?
Choose an answer to see the explanation.
Question 7 of 7
JSON.parsereads this JSON string. What does the code print?Read the code, then predictconst 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
yflag makeslastIndexthe exact place where the next token must start. A failed match withgorysets 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.