Finding ·
IFERROR is hiding a broken regex in your spreadsheet
Somewhere in a monthly report, there is a column that has been empty since the day it was built.
Not empty because there was nothing to find. Empty because the formula never worked at all — and
somebody, trying to be tidy, wrapped it in IFERROR so the red triangles would stop showing up in
the summary tab.
This is not a hypothetical. It is a direct consequence of two facts about Google Sheets that almost nobody puts together, and we ran both of them in a real spreadsheet before writing this sentence.
Two different failures, one identical blank
A regular expression can fail in a Google Sheet for two completely unrelated reasons.
The pattern is invalid. Sheets runs RE2, which has no
lookahead, no lookbehind, and no backreferences. A pattern like (?<=Invoice )\d+ — the natural way
to grab the number after a word, and a pattern that works in every online regex tester — cannot be
parsed at all. The cell returns #REF!.
The pattern is fine, and there is nothing to match. \d+ against no digits here is a perfectly
good pattern asking a perfectly reasonable question with a perfectly reasonable answer: nothing. The
cell returns #N/A.
Those are different errors, with different causes, requiring different responses. Now watch what
IFERROR does to them:
=IFERROR(REGEXEXTRACT("Invoice 4821", "(?<=Invoice )\d+"), "") → (blank)
=IFERROR(REGEXEXTRACT("no digits here", "\d+"), "") → (blank)
The same blank. Byte for byte, cell for cell, indistinguishable.
One of those formulas has never worked, in any row, since the day it was written. The other is
working exactly as intended. From the outside there is no difference, and there never will be, because
IFERROR catches everything and reports nothing.
The one-word fix
IFNA is documented to catch only #N/A. We did not want to assume that “only” meant what it
looked like, so we ran it:
=IFNA(REGEXEXTRACT("Invoice 4821", "(?<=Invoice )\d+"), "") → #REF!
=IFNA(REGEXEXTRACT("no digits here", "\d+"), "") → (blank)
That is the whole answer.
IFNA lets the broken pattern through, so it shows up in the cell as a loud red error you cannot
miss. And it still swallows the honest no-match, which is the only thing you wanted swallowed in the
first place.
Replace IFERROR with IFNA around every REGEXEXTRACT in your spreadsheet. Nothing about the
working formulas changes. The broken ones stop hiding.
Proving it is not the same error wearing a different hat
It would be reasonable to suspect that #REF! and #N/A are somehow the same thing underneath, and
that IFNA is catching one by accident. Sheets has two functions that can settle it, and they agree:
=ISNA(REGEXEXTRACT("Invoice 4821", "(?<=Invoice )\d+")) → FALSE
=ISNA(REGEXEXTRACT("no digits here", "\d+")) → TRUE
=ERROR.TYPE(REGEXEXTRACT("Invoice 4821", "(?<=Invoice )\d+")) → 4
=ERROR.TYPE(REGEXEXTRACT("no digits here", "\d+")) → 7
ERROR.TYPE returns 4 for #REF! and 7 for #N/A. ISNA says the broken pattern is not an
#N/A at all. Two independent measurements, one conclusion: Sheets classifies an unparseable regular
expression as a broken cell reference.
Which explains the twenty minutes you once spent looking for a deleted column that did not exist.
A formula that tells you which failure you have
If you want to audit a sheet without unwrapping every formula by hand, this returns the value when it works, and names the failure when it does not:
=IFERROR(
REGEXEXTRACT(A1, "your pattern"),
IF(ERROR.TYPE(REGEXEXTRACT(A1, "your pattern")) = 4, "BROKEN PATTERN", "no match")
)
We ran all three branches:
| What it was given | What it returned |
|---|---|
(?<=Invoice )\d+ on Invoice 4821 |
BROKEN PATTERN |
\d+ on no digits here |
no match |
Invoice (\d+) on Invoice 4821 |
4821 |
It is not pretty — the pattern appears twice, because ERROR.TYPE needs the error itself, not a
reference to a cell that contains one. Drop it into a spare column, drag it down, and look for the
word BROKEN. Then delete the column.
And then fix the pattern
IFNA and the audit formula tell you where the problem is. They do not solve it. The pattern still
has to be rewritten without the construct RE2 refuses.
Almost always this means turning an assertion into a capture group. Instead of asking for “digits preceded by the word Invoice,” match the whole thing and capture the part you want:
=REGEXEXTRACT("Invoice 4821", "(?<=Invoice )\d+") → #REF!
=REGEXEXTRACT("Invoice 4821", "Invoice (\d+)") → 4821
REGEXEXTRACT returns the contents of the first capture group when there is one, so the surrounding
text is matched and then discarded. This is the standard translation, and it covers most of what
people reach for lookbehind to do.
You can check a pattern against the rules a cell actually enforces with the tester, which was built from these runs.
Where these hide
Three places account for most of the broken patterns we have seen, and they share a shape: someone needed a value that sits next to another value, reached for lookaround, and then tidied up.
Order and invoice numbers. (?<=Order #)\d+ is the first thing anyone writes, because it is how
you would say it out loud. It cannot run. Rewrite as Order #(\d+).
Email addresses out of a text column. \w+(?=@) to get the name before the @. Rewrite as
(\w+)@.
Anything imported. IMPORTRANGE and CSV uploads bring columns whose contents change shape over
time. A pattern that worked in January against INV-4821 returns nothing in June against
INV 4821, and if it is wrapped in IFERROR the report simply gets quieter. A broken pattern and a
changed data source produce the same silence, and only one of them is your fault.
What this costs you
There is a real trade-off in the IFNA recommendation, and it would be dishonest to skip it.
IFERROR was doing something for you. It caught #VALUE! when a cell contained a number instead of
text. It caught #REF! when someone genuinely did delete a column. Swap in IFNA and those come
back — as visible errors in a report you had made clean.
That is the point. A clean report built on a formula nobody has verified is not clean, it is quiet.
If replacing IFERROR with IFNA fills your spreadsheet with red, the red was always there. You were
just not being shown it.
Every claim above, executed
We do not publish a formula we have not run. Below is the actual execution log: each formula was written into a live Google Sheet through the Sheets API, and the third column is the value Google computed. Not what the documentation says, not what a model predicted.
Nine of these results were measured before they were claimed — we did not know whether IFNA
would let #REF! through, so we did not write a word about it until we had asked Google. If any of
this stops being true, tell us, and the correction and its date go on this page.
The run
Nothing above was typed from memory. Here is every formula, and the value Google returned.
Execution log
How a broken regex hides inside IFERROR, and what finds it
| Cell | Formula | What Google returned |
|---|---|---|
| B1 | =IFERROR(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+"),"") IFERROR turns a broken pattern into a blank The pattern uses lookbehind, which Sheets cannot parse. The cell shows nothing at all. | (empty) |
| B2 | =IFERROR(REGEXEXTRACT("no digits here","\d+"),"") IFERROR turns an honest no-match into the same blank A perfectly good pattern with nothing to find. Indistinguishable from the row above. | (empty) |
| B3 | =REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+") the same broken pattern, unwrapped | #REF! |
| B4 | =REGEXEXTRACT("no digits here","\d+") the same honest no-match, unwrapped | #N/A |
| B5 | =IFNA(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+"),"") does IFNA let a broken pattern through? IFNA is documented to catch #N/A only. If #REF! passes through, IFNA is the safe wrapper. | #REF! |
| B6 | =IFNA(REGEXEXTRACT("no digits here","\d+"),"") does IFNA still catch an honest no-match? | (empty) |
| B7 | =ISERROR(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+")) ISERROR sees a broken pattern | TRUE |
| B8 | =ISNA(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+")) ISNA on a broken pattern If this is FALSE, then #REF! is not #N/A, and ISNA can separate the two failures. | FALSE |
| B9 | =ISNA(REGEXEXTRACT("no digits here","\d+")) ISNA on an honest no-match | TRUE |
| B10 | =ERROR.TYPE(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+")) ERROR.TYPE of a broken pattern 4 is #REF!. Established in the re2-limits run. | 4 |
| B11 | =ERROR.TYPE(REGEXEXTRACT("no digits here","\d+")) ERROR.TYPE of an honest no-match 7 is #N/A. | 7 |
| B12 | =IFERROR(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+"),IF(ERROR.TYPE(REGEXEXTRACT("Invoice 4821","(?<=Invoice )\d+"))=4,"BROKEN PATTERN","no match")) an audit formula that names which failure happened — broken pattern | BROKEN PATTERN |
| B13 | =IFERROR(REGEXEXTRACT("no digits here","\d+"),IF(ERROR.TYPE(REGEXEXTRACT("no digits here","\d+"))=4,"BROKEN PATTERN","no match")) the same audit formula — honest no-match | no match |
| B14 | =IFERROR(REGEXEXTRACT("Invoice 4821","Invoice (\d+)"),IF(ERROR.TYPE(REGEXEXTRACT("Invoice 4821","Invoice (\d+)"))=4,"BROKEN PATTERN","no match")) the audit formula leaves a working pattern alone The lookbehind rewritten as a capture group. This is the fix, and it returns the number. | 4821 |
Every row above was written into a real Google Sheet and read back through the Sheets API on 2026-07-09. The case file and this log are in the repository, and the build refuses to run if they disagree.
Execution log
What Google Sheets regex actually supports, and what it silently does instead
| Cell | Formula | What Google returned |
|---|---|---|
| B1 | =REGEXMATCH("abc123","\d+") baseline: REGEXMATCH finds digits Sanity check. If this fails, the harness itself is broken. | TRUE |
| B2 | =REGEXEXTRACT("Order 4821 shipped","\d+") baseline: REGEXEXTRACT pulls the first match | 4821 |
| B3 | =REGEXREPLACE("John Smith","(\w+) (\w+)","$2 $1") baseline: REGEXREPLACE swaps groups with $1 / $2 Dollar-sign backreferences are the Sheets replacement syntax. | Smith John |
| B4 | =REGEXMATCH("abc123","abc(?=123)") RE2 limit: positive lookahead (?=...) Works on regex101. Here it is not #ERROR! — it is #REF!, the error people associate with a deleted cell reference. That is why they hunt for a typo that does not exist. | #REF! |
| B5 | =REGEXMATCH("abc","a(?!b)") RE2 limit: negative lookahead (?!...) | #REF! |
| B6 | =REGEXMATCH("abc123","(?<=abc)123") RE2 limit: lookbehind (?<=...) | #REF! |
| B7 | =REGEXMATCH("abab","(ab)\1") RE2 limit: backreference inside the pattern Backreferences exist in the REPLACEMENT string of REGEXREPLACE, but never in the pattern. | #REF! |
| B8 | =REGEXMATCH("héllo","\p{L}+") Sheets limit: Unicode character class \p{L} RE2 supports this. Google Sheets is the exception Google names in its own docs. This one catches people who already know RE2. | #REF! |
| B9 | =REGEXMATCH("abc","(abc") unbalanced parenthesis is also #REF!, not #ERROR! A malformed regex and an unsupported one produce the same error. The cell tells you nothing about which. | #REF! |
| B10 | =ERROR.TYPE(REGEXMATCH("abc123","abc(?=123)")) cross-check: ERROR.TYPE of an unsupported pattern Independent confirmation of the error type. ERROR.TYPE returns 4 for #REF!, 3 for #VALUE!, 7 for #N/A, 8 for #ERROR!. We do not take one measurement's word for it. | 4 |
| B11 | =ERROR.TYPE(REGEXEXTRACT("abc","\d")) cross-check: ERROR.TYPE of REGEXEXTRACT with no match | 7 |
| B12 | =IFERROR(REGEXMATCH("abc123","abc(?=123)"),"caught") an unsupported pattern is catchable with IFERROR If IFERROR swallows it, a broken pattern can hide inside a working-looking spreadsheet forever. | caught |
| B13 | =REGEXEXTRACT("abc","\d") REGEXEXTRACT with no match A different error from an invalid pattern. People wrap both in IFERROR without noticing. | #N/A |
| B14 | =REGEXMATCH("cat dog","\bdog\b") supported: word boundary \b | TRUE |
| B15 | =REGEXEXTRACT("<a><b>","<(.+?)>") supported: lazy quantifier +? | a |
| B16 | =REGEXMATCH("ABC","(?i)abc") supported: inline case-insensitive flag (?i) | TRUE |
| B17 | =REGEXEXTRACT("x=1","(?P<digit>\d)") supported: RE2 named capture group (?P<name>...) RE2's own spelling. | 1 |
| B18 | =REGEXEXTRACT("x=1","(?<digit>\d)") supported: JavaScript named group syntax (?<name>...) also works Measured, not assumed. Sheets accepts both spellings, and ignores the name either way — REGEXEXTRACT returns groups in order. | 1 |
| B19 | =REGEXREPLACE("John Smith","(\w+) (\w+)","\2 \1") trap: backslash backreference in the replacement string No error. The backslashes vanish and you are left with the literal digits. Worse than an error, because an error tells you to stop. | 2 1 |
| B20 | =REGEXREPLACE("abc","b","[$0]") trap: $0 is the whole match in the replacement string | a[b]c |
| B21 | =REGEXREPLACE("abc","b","[$1]") trap: $1 when the pattern has no capture group It is an error, not a silent empty string. We did not know, so we measured. | #N/A |
Every row above was written into a real Google Sheet and read back through the Sheets API on 2026-07-09. The case file and this log are in the repository, and the build refuses to run if they disagree.
When a new pattern gets verified, I'll tell you
One email when a new formula is executed, logged, and published. Nothing else. Unsubscribe in one click.