[{"data":1,"prerenderedAt":38},["ShallowReactive",2],{"$fTYcOWdsUwOiQyywLb8OCz0_Q5d2A-SCzVnwYqaZt3bE":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"tools":13,"draft":15,"html":16,"toc":17,"minutes":37},"regex-tester-catastrophic-backtracking","How My Regex Tester Survives Catastrophic Backtracking","Why one bad regular expression can freeze a browser tab for minutes, and how ToolOnTap's regex tester runs patterns in a worker it can stop after 1.5 seconds.","2026-10-16",[9,10,11,12],"regex","javascript","web workers","performance",[14],"regex-tester",false,"\u003Cp>A name field on a form accepts “Mohamed Abdelrahman Elsayed Ibrahim” in an instant. Add an exclamation mark at the end and the same check takes about 24 seconds on my laptop. The pattern behind the field is \u003Ccode>^(\\w+\\s?)*$\u003C\u002Fcode>, and it has a problem called catastrophic backtracking.\u003C\u002Fp>\n\u003Cp>When I built ToolOnTap’s \u003Ca href=\"\u002Fregex-tester\">regex tester\u003C\u002Fa>, this was the case I worried about most. A tester runs whatever pattern you type, on every keystroke, against whatever text you paste. If one of those runs takes 24 seconds on the main thread, the whole tab freezes with it. This article explains why that happens and how the tester stays usable anyway.\u003C\u002Fp>\n\u003Ch2 id=\"why-a-regex-can-take-minutes\">Why a regex can take minutes\u003C\u002Fh2>\n\u003Cp>JavaScript’s regex engine is a backtracking engine. When part of a pattern can match in more than one way, the engine picks one, carries on, and if the rest fails, it comes back and tries the next way.\u003C\u002Fp>\n\u003Cp>Nested repetition changes that. Take the shortest version of the problem, \u003Ccode>^(a+)+$\u003C\u002Fcode>, and the text \u003Ccode>aaaaaaaaaaaaaaaaaaaa!\u003C\u002Fcode>. The inner \u003Ccode>a+\u003C\u002Fcode> can take any number of a’s, and the outer \u003Ccode>+\u003C\u002Fcode> can repeat it any number of times. So a run of 20 a’s can be split into groups in 2¹⁹ ways, which is 524,288: one group of 20, a group of 19 and a group of 1, and so on. The \u003Ccode>!\u003C\u002Fcode> at the end means \u003Ccode>$\u003C\u002Fcode> can never match, so the engine tries every split before it gives up.\u003C\u002Fp>\n\u003Cp>Every extra \u003Ccode>a\u003C\u002Fcode> doubles the number of splits. That’s what I saw when I timed it in Node 20:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>Text\u003C\u002Fth>\n\u003Cth>Result\u003C\u002Fth>\n\u003Cth>Time on my laptop\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>20 a’s and \u003Ccode>!\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>no match\u003C\u002Ftd>\n\u003Ctd>a few to tens of milliseconds\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>24 a’s and \u003Ccode>!\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>no match\u003C\u002Ftd>\n\u003Ctd>under a second\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>30 a’s and \u003Ccode>!\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>no match\u003C\u002Ftd>\n\u003Ctd>several seconds to more than 30 seconds\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>30 a’s, no \u003Ccode>!\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>match\u003C\u002Ftd>\n\u003Ctd>about 0.01 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>The last row is the important one. The pattern is fast when the text matches. It explodes only when the text \u003Cstrong>almost\u003C\u002Fstrong> matches, which is why this kind of bug passes tests and then shows up in production with one odd input.\u003C\u002Fp>\n\u003Cp>The name-field pattern from the start has the same shape. \u003Ccode>\\w+\u003C\u002Fcode> repeats inside \u003Ccode>(…)*\u003C\u002Fcode>, and \u003Ccode>\\s?\u003C\u002Fcode> is optional, so a word like “Abdelrahman” can be cut into pieces in many ways. A version that gives each part a distinct job, \u003Ccode>^\\w+(?:\\s\\w+)*$\u003C\u002Fcode>, rejects the same text with the \u003Ccode>!\u003C\u002Fcode> in about 0.02 ms. Every space now has to be a real space between two words, so there’s only one way to read the text.\u003C\u002Fp>\n\u003Ch2 id=\"a-regex-can-t-be-stopped-halfway\">A regex can’t be stopped halfway\u003C\u002Fh2>\n\u003Cp>My first idea was to measure time inside the matching loop and give up after a limit. That doesn’t work. \u003Ccode>RegExp.prototype.exec\u003C\u002Fcode> is one synchronous call. Once it starts, no JavaScript runs on that thread until it returns, so no timer fires and no flag gets checked. There is no option to pass to the engine that limits its steps either.\u003C\u002Fp>\n\u003Cp>What you \u003Cem>can\u003C\u002Fem> stop is a whole thread. A Web Worker runs its code on a separate thread, and the page can call \u003Ccode>terminate()\u003C\u002Fcode> on it at any time, even in the middle of a regex. The worker dies, the page carries on.\u003C\u002Fp>\n\u003Cp>So the tester never runs your pattern on the page itself. The worker, \u003Ccode>workers\u002Fregex.worker.ts\u003C\u002Fcode>, is short. It builds the regex, finds the matches, runs the replacement if Replace is on, and posts everything back with how long it took:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">ctx.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">onmessage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> ({ \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">data\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">job\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=>\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> built\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> buildRegex\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(job.pattern, job.flags)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">built.ok) { ctx.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">postMessage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ id: job.id, ok: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">false\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, error: built.error }); \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> start\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> performance.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">now\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">matches\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">total\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> } \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> findMatches\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(built.regex, job.text, job.limit)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> replaced\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> job.replacement \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">===\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> ?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> :\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> replaceText\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(built.regex, job.text, job.replacement)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  ctx.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">postMessage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ id: job.id, ok: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">true\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, matches, total, replaced, ms: performance.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">now\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> start })\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>That \u003Ccode>ms\u003C\u002Fcode> value is the time you see next to the match count.\u003C\u002Fp>\n\u003Ch2 id=\"the-1-5-second-limit\">The 1.5 second limit\u003C\u002Fh2>\n\u003Cp>The other half lives in \u003Ccode>composables\u002FuseRegexRunner.ts\u003C\u002Fcode>. It sends each job to the worker and starts a timer next to it:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">    clearTimeout\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(timer)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    timer \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> setTimeout\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(() \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=>\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">      if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (id \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">!==\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> seq) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">      stop\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">      reply.value \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">      status.value \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'timeout'\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    }, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">TIME_LIMIT\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    worker.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">postMessage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">...\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">job, id })\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>TIME_LIMIT\u003C\u002Fcode> is 1500 milliseconds. If the worker answers in time, the timer is cleared and the results show. If not, \u003Ccode>stop()\u003C\u002Fcode> terminates the worker and the page says: “Stopped after 1.5 seconds: this pattern backtracks too much on this text. Look for nested repetition such as (a+)+ or overlapping alternatives.”\u003C\u002Fp>\n\u003Cp>A few details took more thought than the timer itself:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Every job has a number.\u003C\u002Fstrong> \u003Ccode>seq\u003C\u002Fcode> goes up with each new job, and a reply whose \u003Ccode>id\u003C\u002Fcode> isn’t the latest one is ignored. Without it, a slow answer for the pattern you typed two keystrokes ago could replace the answer for the pattern you have now.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A new keystroke kills a busy worker.\u003C\u002Fstrong> If a job is still running when the next one arrives, the runner terminates the worker first. Waiting for a stuck job to finish would make every later keystroke wait too.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The next job gets a fresh worker.\u003C\u002Fstrong> After \u003Ccode>terminate()\u003C\u002Fcode> the worker is gone for good, so \u003Ccode>run()\u003C\u002Fcode> creates a new one the next time.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The worker loads on first use.\u003C\u002Fstrong> It’s imported with \u003Ccode>?worker\u003C\u002Fcode> the first time you change something, so it ships as its own plain \u003Ccode>.js\u003C\u002Fcode> file and isn’t part of the page’s first download. The example you see when the page opens, a date pattern, is matched once during rendering, because it’s my own pattern and known to be fast.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Typing also waits 120 ms after your last keystroke before it sends a job, so typing a word doesn’t start and kill ten workers.\u003C\u002Fp>\n\u003Cp>1.5 seconds is a judgment call. It’s long enough that a heavy but healthy pattern on a big log file finishes, and short enough that you don’t wonder whether the page has crashed.\u003C\u002Fp>\n\u003Ch2 id=\"big-inputs-limits-on-what-comes-back\">Big inputs: limits on what comes back\u003C\u002Fh2>\n\u003Cp>A fast pattern can still produce too much output. \u003Ccode>\\w\u003C\u002Fcode> with the g flag on a long document gives a match for every word. So the worker counts every match but keeps the details (position and groups) of the first 1,000 only. The table lists 200, and highlighting covers the first 50,000 characters of the text. The match count at the top is still the real total.\u003C\u002Fp>\n\u003Ch2 id=\"reading-a-pattern-in-plain-english\">Reading a pattern in plain English\u003C\u002Fh2>\n\u003Cp>Stopping a slow pattern is half of the job. The other half is helping you see \u003Cem>why\u003C\u002Fem> it’s slow, and for that the tester explains the pattern piece by piece. The explainer in \u003Ccode>utils\u002Fregex.ts\u003C\u002Fcode> is a small hand-written scanner. It walks the pattern once, keeps track of how deep it is inside groups, and attaches each quantifier to the piece before it. For the pattern above it gives:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>^ = Start of the text\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>(…)+ = Group 1 (captured), repeated one or more times, as many as possible\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  a+ = The character a, one or more times, as many as possible\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>) = End of group\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>$ = End of the text\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Seen this way, the problem is easy to spot. A line that says “one or more times” sits inside a group that is also “repeated one or more times”. That’s the shape to look for. The explainer doesn’t try to detect it automatically, because a nested quantifier is only dangerous when the inner and outer parts can match the same characters, and telling those cases apart reliably is much harder than spotting them by eye.\u003C\u002Fp>\n\u003Cp>A few rules in the scanner exist because the regex syntax is less regular than it looks:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>A quantifier after plain text applies only to the last character, so \u003Ccode>abc?\u003C\u002Fcode> reads as “the text ab” then “the character c, optional”.\u003C\u002Fli>\n\u003Cli>A quantifier after \u003Ccode>)\u003C\u002Fcode> repeats the whole group, so it’s shown on the group’s first line.\u003C\u002Fli>\n\u003Cli>\u003Ccode>\\b\u003C\u002Fcode> means a word boundary outside square brackets and a backspace character inside them.\u003C\u002Fli>\n\u003Cli>\u003Ccode>^\u003C\u002Fcode> and \u003Ccode>$\u003C\u002Fcode> read as “start of a line” and “end of a line” when the m flag is on.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"what-the-tester-can-t-promise\">What the tester can’t promise\u003C\u002Fh2>\n\u003Cp>The tester uses your browser’s own engine, so a pattern that passes here behaves the same in your front-end code and, with the same engine, in Node.js. The flip side is that a pattern can be fast in one engine and slow in another, and a time limit only protects the page. If your server runs the same pattern on user input, it has no such limit, and a single request can keep a thread busy. When the tester stops your pattern, treat it as a bug report for your code.\u003C\u002Fp>\n\u003Cp>You can try all of this, including \u003Ccode>^(a+)+$\u003C\u002Fcode>, in the \u003Ca href=\"\u002Fregex-tester\">regex tester\u003C\u002Fa>. The other developer tools are on the \u003Ca href=\"\u002Fdev-tools\">developer tools\u003C\u002Fa> page.\u003C\u002Fp>\n",[18,22,25,28,31,34],{"id":19,"text":20,"level":21},"why-a-regex-can-take-minutes","Why a regex can take minutes",2,{"id":23,"text":24,"level":21},"a-regex-can-t-be-stopped-halfway","A regex can’t be stopped halfway",{"id":26,"text":27,"level":21},"the-1-5-second-limit","The 1.5 second limit",{"id":29,"text":30,"level":21},"big-inputs-limits-on-what-comes-back","Big inputs: limits on what comes back",{"id":32,"text":33,"level":21},"reading-a-pattern-in-plain-english","Reading a pattern in plain English",{"id":35,"text":36,"level":21},"what-the-tester-can-t-promise","What the tester can’t promise",6,1792325800849]