Regex Inline Flags Killed the Field: A Pattern That Compiles Everywhere Else

Date:

The question was not broken, not hidden, and not misconfigured. It was disabled — by a two-character prefix that every other language I use accepts without complaint.

The short answer: If a form builder’s field goes completely dead — the cursor will not enter it, no error appears, nothing you type registers — check the response-validation pattern first. A pattern beginning with the inline flag (?i) is valid in Python, Go and most server-side engines, but the browser compiles validation with JavaScript’s RegExp, which rejects the bare flag prefix as Invalid regular expression: Invalid group. Handed a rule it cannot compile, the form does not warn and does not fall back to no validation — it renders the question disabled. The fix is one line: spell case-insensitivity out in character classes. The lesson generalizes: a regex is only valid in the dialect of the engine that runs it, not the one you wrote it in.

Regex dialect mismatch is a class of bug where a pattern is syntactically valid in the author’s language and invalid in the engine that ultimately executes it, so the failure appears at runtime in a system far from where the pattern was written.

The symptom: a field nobody could type in

The report was four words long: the email field is unresponsive.

Not “rejects my address”, not “shows an error” — unresponsive. Clicking it did nothing. The cursor never appeared. Every other field on the form behaved normally, including one immediately above it with its own validation rule on a phone number.

That detail mattered more than it looked. A form-wide problem would have killed every field. A rule that was merely wrong would have accepted input and then complained. This was one field, dead on arrival, with a working sibling of the same type two inches above it.

A validation rule that rejects your input is working. A validation rule that removes the input is broken.

Step 1 — “The user is mistaken”

The first hypothesis is always that the field is fine and the report is imprecise. It costs nothing to check and it is right often enough to be worth the minute.

It was wrong here. The field was genuinely inert.

Step 2 — “Something is covering it”

An overlay, a stray element, a layout bug at that viewport — a plausible cause of “clicking does nothing” that has nothing to do with the field itself.

Reading the rendered element settled it. The input carried, in its own markup:

disabled="" aria-disabled="true"Code language: JavaScript (javascript)

Nobody had disabled it. Nothing in the form definition asked for that. But the browser had rendered it that way, which reframed the question entirely: this was not a field failing to receive input, it was a field the platform had deliberately switched off.

Step 3 — “A platform setting did this”

Forms have settings that make fields behave strangely — collecting the respondent’s email automatically, limiting responses per account, restricting to a sign-in. Any of these might plausibly render an email question inert.

This is where a comparison saves an hour. The phone field, sitting in the same section with the same question type and its own validation pattern, was not disabled:

phone: aria-disabled="false"
email: disabled="" aria-disabled="true"Code language: JavaScript (javascript)

Two fields, one form, one setting environment. Whatever caused this was a property of the email question, not of the form.

Step 4 — Read what the platform stored

The only remaining difference between the two questions was the validation rule itself. Pulling the form’s own stored definition showed the pattern intact and exactly as written — the platform had accepted it and saved it without complaint.

That is the detail that makes this bug expensive. The pattern was accepted at authoring time. No warning, no rejection, no red text. It failed only when a browser later tried to compile it.

The two patterns, side by side:

phone  ^[6-9][0-9]{9}$
email  (?i)^[a-z0-9._%+-]+@(example\.com|([a-z0-9-]+\.)*example\.org)$

One of these is not like the other, and it is not the length.

Step 5 — The probe

Every hypothesis so far had been eliminated by observation. This one needed a test, and the right test was almost embarrassingly direct: compile both patterns in the same engine the browser uses, and see which one survives.

new RegExp("^[6-9][0-9]{9}$");   // fine
new RegExp("(?i)^[a-z0-9._%+-]+@example\\.com$");
// SyntaxError: Invalid regular expression: Invalid group
Code language: JavaScript (javascript)

There it was. Not a subtle mismatch, not an edge case — the pattern would not compile at all. The browser had been handed a rule it could not build, and its response was to disable the question rather than ship a field with a validation promise it could not keep.

The platform accepted the pattern at authoring time and rejected it at render time. Only one of those told me.

Why (?i) is portable-looking and not portable

The (?i) prefix sets a flag for the rest of the pattern. Python, Go’s RE2, PCRE and most server-side engines accept it. It looks like regex punctuation rather than a language feature, which is exactly why it travels into places it does not belong.

JavaScript has never supported that bare flag-prefix form. It expects flags as an argument — new RegExp(pattern, "i") — a slot that a form builder’s validation box does not offer you.

The modern nuance is worth knowing, because it makes the failure more confusing rather than less. Current JavaScript engines do support scoped modifiers — the (?i:...) form that applies a flag to one group:

PatternPython / RE2JavaScript (current)
(?i)^abc$ — bare prefixcompilesthrows Invalid group
^(?i:abc)$ — scoped modifiercompilescompiles
new RegExp("^abc$", "i")n/acompiles

So “JavaScript doesn’t do inline flags” is now wrong as a blanket statement — it does the scoped form and not the prefix form. Reading a changelog announcing modifier support is exactly the kind of thing that would convince an author the prefix is safe. It is not.

<!– figure: regex dialect matrix – which inline-flag forms compile in which engine –>

The fix, and why it is uglier than it should be

With no flags argument available and scoped modifiers depending on the visitor’s browser version, the portable answer is the blunt one — write the case-insensitivity into the pattern:

^[A-Za-z0-9._%+-]+@([Gg][Mm][Aa][Ii][Ll]\.[Cc][Oo][Mm]|([A-Za-z0-9-]+\.)*[Gg][Oo][Vv]\.[Ii][Nn])$Code language: CSS (css)

It is unlovely and it is correct in every engine, which is the trade a validation box forces on you. The field came back immediately.

Two things are worth guarding after a fix like this. First, escaping survives the round trip: in a scripted form builder the pattern is a string in a string, so \\. in source becomes \. in the rule. Get that wrong and \. silently degrades to ., which matches any character — a pattern that still compiles, still looks right, and quietly accepts gmailXcom. Second, verify by compiling, not by reading. Reading a regex proves nothing; the engine is the only authority on whether it builds.

What would have caught this sooner

Nothing in the authoring flow was going to catch it, which is the uncomfortable part. The platform accepted the pattern, saved it, and reported success. The only signal was a rendered attribute on a live page.

The generalizable habits are small:

  1. Know which engine runs your pattern. Not the language you wrote it in — the one that executes it. For anything client-side, that is the browser’s RegExp.
  2. Compile-test any pattern that crosses a language boundary. One line, one second, before it ships.
  3. Treat “the control went missing” as a configuration error, not a UI bug. A disabled input, a vanished button, a section that no longer renders — platforms routinely respond to an invalid rule by removing the thing it governs. This is the same shape as a CI check that passes because it runs nothing: the absence of a complaint is not evidence that the rule is working.
  4. Compare against a working sibling. The phone field eliminated three hypotheses in ten seconds, and it did so because it was identical in every respect but the one that mattered. When a system offers you a natural control, use it before you start reasoning — the same discipline that turned a REST API 401 on one subsite into a single decisive probe.

Reading a regex proves nothing. Compiling it is the only opinion that counts.

The short version

A form field rendered disabled with no error because its validation pattern began with (?i) — an inline flag prefix Python and RE2 accept and JavaScript’s RegExp rejects as Invalid group. The builder saved the pattern happily; only the browser, compiling it at render time, refused. A working sibling field eliminated every environmental hypothesis, and compiling both patterns in the browser’s own engine settled it in ten seconds. The fix is character classes instead of a flag prefix — ugly, portable, correct. The habit worth keeping: compile-test any pattern that crosses a language boundary.

Frequently Asked Questions

Why does the form disable the field instead of showing an error?

Because the failure happens while the page is being built, not while it is being used. The validation rule is compiled as the question renders; when compilation throws, the platform has a question it cannot police, and disabling it is the conservative choice. A visible error would require the platform to anticipate an author mistake it has already accepted and stored.

Is (?i) invalid in JavaScript?

The bare prefix form is. new RegExp("(?i)abc") throws Invalid regular expression: Invalid group. Current engines do accept the scoped form (?i:abc), so the blanket claim that JavaScript has no inline modifiers is out of date — but the prefix form used by Python and RE2 has never worked and still does not.

How do I make a pattern case-insensitive without inline flags?

Three options, in order of portability. Write the alternatives into character classes — [Gg][Mm] — which works in every engine. Use the scoped modifier (?i:...) if you control the runtime and know it is current. Or pass the flag as an argument, new RegExp(pattern, "i"), when you have access to the call rather than just a pattern box.

Why did the pattern save without a warning?

Because saving and compiling happen in different places. The authoring interface stores the pattern as text; the browser compiles it later when a respondent loads the form. Nothing in the storage path evaluates whether the pattern builds, which is why an invalid rule can sit in a form definition indefinitely looking perfectly healthy.

How can I check a validation pattern before publishing a form?

Open the browser console and run new RegExp("<your pattern>"). If it throws, the pattern will fail in the form too. It is worth also testing the values you intend to accept and reject — a pattern that compiles can still be wrong, and a stray escaping error can turn a literal dot into a wildcard that quietly accepts almost anything.

Does this affect anything other than form builders?

Any place a pattern is authored in one language and executed in another: validation attributes rendered into HTML, rules stored in a database and applied client-side, configuration files consumed by a different runtime, or a pattern copied from documentation written for another engine. The form builder is just where the failure is most visible, because the platform’s response is to remove the control entirely.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.