=FormulaProof
1

Finding ·

IFERROR is hiding a broken regex in your spreadsheet

2

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.

3

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

Ran
Engine
Google Sheets (live, via Sheets API v4)
Locale
en_US
Host
Windows_NT 10.0.26200 · Node v24.15.0
Result
14 of 14 as expected
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

Ran
Engine
Google Sheets (live, via Sheets API v4)
Locale
en_US
Host
Windows_NT 10.0.26200 · Node v24.15.0
Result
21 of 21 as expected
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.

4