Skip to content

[Due for payment 2026-10-13] [Sentry: APP-B65] No fallback when indexedDB is unavailable, causing a rejection retry loop #100844

Closed

Description

@mountiny

Sentry

Impact (snapshot at filing, 2026-09-10)

Sentry ID Users (last 14d) Users (total since first seen) Events First seen
APP-B65 8 24 16,934 2026-04-24
  • Last seen: 2026-09-10 (ongoing)
  • Platform: web (standalone), Chrome Mobile on iOS
  • App version(s): present across 9.4.61-4 through 9.4.74-2 — not a recent regression
  • Mechanism: auto.browser.global_handlers.onunhandledrejection, level error
  • Affected view(s): not view-specific; the sampled event landed on Settings_Rules

The event-to-user ratio is the important number here: roughly 700 events per affected user. A single session produces hundreds of rejections, so this issue alone accounts for a large share of total error volume even though few users hit it.

Error

ReferenceError: Can't find variable: indexedDB

Stack trace (top frames)

app:///10042537-100413459.js:5095:107328 (lu)
app:///10042537-100413459.js:5095:108205 (tg)
app:///10042537-100413459.js:5095:8423 (S)
app:///10042537-100413459.js:5095:158371 (Xp)
app:///10042537-100413459.js:5095:162388 (run)
[native code] (Promise)

The frames are minified. Source maps are not resolving for this bundle, so there are no readable function names — see the second task below.

Suspected cause

The storage layer assumes window.indexedDB exists. On Chrome for iOS the global is absent in some configurations — private browsing and Lockdown Mode are the known cases — so the very first storage call throws a ReferenceError rather than a rejected open request.

There is no fallback and no cached result of the failure, so every subsequent read and write throws the same way. That produces the retry loop visible in the event count. The user is left on an app that renders but cannot persist anything, with no message explaining why.

Two separate pieces of work:

  1. Detect and degrade. Probe for indexedDB once at storage init. When it is missing, fall back to the in-memory or localStorage provider that Onyx already supports, record the decision once, and stop retrying. Surface a message telling the user that data will not persist in this browser mode.
  2. Fix source maps for this bundle. The stack has no first-party frames, which is why this sat untriaged. Worth confirming the web release upload step is attaching maps for all bundles, not just the main chunk. This overlaps with Upload Android NDK debug symbols, which covers the native side of the same gap.

Reproduction

  1. Open Chrome on iOS and switch to a private tab, or enable Lockdown Mode.
  2. Load https://new.expensify.com and sign in.
  3. Open the browser console and verify repeated Can't find variable: indexedDB rejections.
  4. Verify that no error message is shown to the user, and that state does not survive a reload.

Related

Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on Sep 10, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 10, 2026
  3. melvin-bot commented on Sep 10, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @getusha (External)

  4. melvin-bot commented on Sep 10, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  5. MelvinBot commented on Sep 10, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    Onyx has a "degrade to memory-only storage" fallback, but on this failure mode it is unreachable dead code.

    Storage.init() only degrades when provider.init() throws the exact string IDBKeyVal store could not be created (lib/storage/index.ts:43-44). That throw fires only when createStore() returns null (IDBKeyValProvider/index.ts:40-45). But Onyx's createStore() is lazy — it never touches indexedDB and unconditionally returns a closure, so it can never return null. Init always "succeeds", even in a browser with no IndexedDB at all.

    indexedDB is first read much later, on the first real operation, as a bare unguarded global (createStore.ts:59). On WebKit (Chrome for iOS is WKWebView) the global is genuinely absent in private/Lockdown configurations, so this is a ReferenceError, not a rejected open request — the message Can't find variable: indexedDB is WebKit's wording for an undeclared identifier, which rules out "IndexedDB present but broken".

    That error class is then unrecognised everywhere downstream, which is what turns one failure into 16,934 events:

    • classifyIDBError has no branch for it, so it falls through to UNKNOWN (classifyError.ts:49).
    • UNKNOWN means "bounded retry" — 5 retries per write, MAX_STORAGE_OPERATION_RETRY_ATTEMPTS (OnyxUtils.ts:854-865) — so every write costs 6 rejections and none of them ever succeed.
    • Reads are worse: Storage.getAllKeys() / getItem / multiGet re-reject with no .catch at their call sites (for example OnyxUtils.ts:476), which is exactly the onunhandledrejection mechanism Sentry tagged.
    • The provider is never swapped, so this repeats for the entire session. Hence ~700 events per user and no user-facing message.

    What changes do you think we should make in order to solve the problem?

    The fix belongs in Expensify/react-native-onyx, not in App — App only needs the version bump afterwards (currently pinned to react-native-onyx@3.0.111 in package.json:203).

    1. Probe once, at init. In IDBKeyValProvider.init(), check typeof indexedDB === 'undefined' (a typeof guard, so it does not itself throw) before returning the store, and throw the sentinel the existing fallback already listens for. This makes degradePerformance() fire on the very first call and swap the provider to MemoryOnlyProvider — no new fallback machinery needed, just making the existing one reachable.
    2. Guard the lazy path too. Add the same typeof guard inside getDB() so a mid-session loss can't produce the bare ReferenceError.
    3. Classify it so it can never storm again. Give classifyIDBError an explicit branch for a missing/unavailable IndexedDB that returns a non-retryable class. Today an unrecognised error costs 6 attempts, so any future variant of this bug re-amplifies 6× before anyone notices.
    4. Tell the user. Once degraded, surface a one-time message that data will not persist in this browser mode.

    Separately, the source-map claim in the issue doesn't hold up — worth splitting out rather than fixing here. The web build already uploads maps for every chunk, not just the main one (assets: './dist/**/*.{js,map}', rsbuild.common.ts:579-580), and the build-time and runtime release strings match (${pkg.name}@${pkg.version} in both rsbuild.common.ts:570 and setupSentry.ts:59). The stack frame 10042537-100413459.js does not match the current production naming scheme [name]-[contenthash].bundle.js (rsbuild.common.ts:377), so these frames most likely come from cached or pre-rsbuild bundles rather than a broken upload step. Confirm against a recent release before spending work here.

    What alternative solutions did you explore? (Optional)

    Alternatives considered and ruled out
    • Falling back to localStorage instead of memory (as the issue suggests). Chrome iOS incognito also restricts localStorage, so it would need the same guard and would mostly reintroduce the problem. MemoryOnlyProvider already exists and is what the current fallback path targets — use it.
    • App-side fix. App has one other unguarded indexedDB reference at src/libs/ExportOnyxState/index.ts:11. It's worth guarding, but it is Troubleshoot-page-only and cannot produce hundreds of events per session, so it is not this issue.
    • IndexedDB present but failing to open. Ruled out — WebKit's Can't find variable: X is a ReferenceError for an undeclared identifier. A present-but-broken IDB would surface as a rejected open request with a DOMException.
    • Error originating in a worker context. Ruled out — createStore calls document.addEventListener('visibilitychange', …), which would itself throw in a worker.

    Related issues

    Not duplicates — none of them cover the missing-indexedDB case or the unreachable fallback.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/100844) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  6. sosek108 commented on Sep 15, 2026

    @sosek108
    Contributor

    The fix proposed by Melvin is proper one. There is my draft PR with implementation Expensify/react-native-onyx#845.

    Should we also tell the user about the situation? I don't recall any popup informing about storage errors in the App so I'm not sure

    Tell the user. Once degraded, surface a one-time message that data will not persist in this browser mode.

    cc: @mountiny @fabioh8010

  7. sosek108 commented on Sep 17, 2026

    @sosek108
    Contributor
  8. melvin-bot commented on Sep 22, 2026

    @melvin-bot

    @sosek108 Huh... This is 4 days overdue. Who can take care of this?

  9. 17 remaining items

  10. added and removed on Oct 8, 2026
  11. mountiny commented on Oct 8, 2026

    @mountiny
    ContributorAuthor

    All right, let's monitor that this issue has been fixed. Thank you!

  12. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Oct 12, 2026
  13. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  14. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  15. mallenexpensify commented on Oct 15, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @getusha due $250 via NewDot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions