Skip to content

[Due for payment 2026-10-08] [Sentry: APP-M75 / APP-M74 / APP-M8W] Web Safari crash, Skia Host.dispose calls an undefined this[Symbol.dispose] #102043

Closed

Description

@mountiny

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.90
Reproducible in staging?: Needs to be confirmed
Reproducible in production?: Yes — crash is live in production
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A — found via Sentry production crash scan
Email or phone of affected tester (no customers): N/A
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: Sentry production crash scan
Slack conversation (hyperlinked to channel name): N/A

Action Performed:

  1. Open New Expensify in Safari on macOS.
  2. Navigate to a page that renders a Skia canvas — the reported route is the Expensify Card details page at /workspaces/:policyID/members/:accountID/card-details/:cardID.
  3. Let the canvas draw a frame.

Expected Result:

The Skia frame draws, disposes its resources, and no error is thrown.

Actual Result:

Every drawn frame throws:

TypeError: this[Symbol.dispose] is not a function.
(In 'this[Symbol.dispose]()', 'this[Symbol.dispose]' is undefined)

The failure happens in the Skia teardown path, so it repeats on each frame. The event-to-user ratio is very high: 769 events across 23 users.

Workaround:

Unknown. Using Chrome instead of Safari avoids the reported events.

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Sentry details

Sentry issues:

Impact (snapshot at filing, 2026-09-23):

  • Users (last 7d, all three IDs): 23
  • Events (last 7d, all three IDs): 769
  • First seen: 2026-09-21
  • Last seen: ongoing
  • Platform: web, Safari 18.6 on macOS
  • Affected routes (parameterized): /workspaces/:policyID/members/:accountID/card-details/:cardID, /home
  • Mechanism: auto.browser.global_handlers.onerror
  • Substatus: escalating

Stack trace (innermost last):

react-native-worklets/lib/module/threads.js:50:13
react-native-worklets/lib/module/threads.js:51:24   const result = workletFunction(...workletArgs)
@shopify/react-native-skia/lib/module/sksg/Container.web.js:113:7  drawOnscreen(Skia, nativeId, recording)
@shopify/react-native-skia/lib/module/sksg/Container.web.js:35:9   ctx.dispose()
@shopify/react-native-skia/lib/module/sksg/Recorder/FrameScope.js:49:22  disposables[i].dispose()
@shopify/react-native-skia/lib/module/skia/web/Host.js:25:25       this[Symbol.dispose]()

Suspected cause: Host.js implements dispose() by delegating to this[Symbol.dispose](), and on these clients the instance has no Symbol.dispose method. Two things are worth checking:

  1. Whether the Safari build in use supports explicit resource management well enough for the Symbol.dispose well-known symbol to be present, and whether a polyfill is needed before Skia loads.
  2. Whether the disposable object reaching FrameScope is one that never defined [Symbol.dispose], which would make this a Skia bug rather than a browser support gap.

Because the throw is in the per-frame teardown loop, a guard at FrameScope.js:49 that skips entries without a callable disposer would stop the bleeding while the real cause is found.

Related: filed alongside the CanvasKit wasm link failure found in the same scan. Both are @shopify/react-native-skia web issues that appeared on 2026-09-21, so a recent Skia or canvaskit-wasm bump is a good place to start for both.

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on Sep 23, 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 23, 2026
  3. melvin-bot commented on Sep 23, 2026

    @melvin-bot

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

  4. melvin-bot commented on Sep 23, 2026

    @melvin-bot

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

  5. mountiny commented on Sep 23, 2026

    @mountiny
    ContributorAuthor

    @WojtekBoman can you take a look I think this might have become more frequent after the bump #100953

  6. MelvinBot commented on Sep 23, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    Skia 2.11.x implements disposal on web as a delegation through the Symbol.dispose well-known symbol:

    Host.js:24-27 (as shipped in node_modules/@shopify/react-native-skia/lib/module/skia/web/Host.js)

    dispose() {
        this[Symbol.dispose]();   // <-- line 25, the throw
    }
    [Symbol.dispose]() { /* real implementation */ }

    Safari does not ship Symbol.dispose. MDN's browser-compat-data records chrome: 125, firefox: 141, safari: "preview" — Explicit Resource Management is still Technology Preview only (WebKit STP 250 notes). So on Safari 18.6, Symbol.dispose is undefined.

    That fact alone is survivable, and this is the subtle part: a computed key runs through ToPropertyKey, so [Symbol.dispose]() {} in the class body and this[Symbol.dispose] at the call site both coerce undefined to the string key "undefined" — they still match.

    It only breaks when the two evaluations disagree. The class key is computed once, when Host.js is evaluated; the call site re-reads Symbol.dispose on every call. If anything defines the real symbol after Skia's module has loaded, the method is stranded under the string key "undefined" while dispose() now looks up a genuine symbol — undefined — and throws. This is exactly why the crash is Safari-only: on Chrome/Firefox the symbol is native from the very start, so the keys can never diverge.

    New Expensify loads several third-party scripts asynchronously after the bundle starts (web/index.html:39-46,159-164 — Ketch, Convert, GTM), plus the Group-IB SDK. Any vendor bundle built with core-js at the esnext level installs Symbol.dispose globally on load. That is the most likely late definer, though I could not confirm which one without a Safari session.

    Why it repeats every frame, and why the card-details URL is in the report

    Per-frame loop. drawOnscreen disposes frame-scoped objects in a finally block on every draw (sksg/Container.web.js:31-37), which walks disposables[i].dispose() in sksg/Recorder/FrameScope.js:47-51, hitting Host.js:25 for each one. That accounts for the 769 events / 23 users ratio.

    The routes. /home really does mount a Skia surface: HomePage.tsx:100 → InsightsSectionContent.tsx:108 → SearchChartView.tsx:45-47 → SkiaWebChart/index.tsx:86.

    The card-details page mounts no Skia at all — I read DynamicWorkspaceExpensifyCardDetailsPage.tsx in full; the card artwork is a static SVG. Covered screens stay mounted under <Activity mode="hidden"> (ScreenActivityWrapper/index.tsx:78-83), so the Home chart keeps drawing behind the RHP and Sentry attributes the throw to whatever URL is current.

    The existing guard does not cover this. useIsSkiaSurfaceUnavailable only fires when a surface cannot be created. This crash happens after a surface exists, during teardown, so it is structurally blind to it.

    Regression window — pinned to a specific PR and deploy

    Bump @shopify/react-native-skia and drop the patch (Expensify/App#100953, merged 2026-09-18) moved @shopify/react-native-skia from ^2.4.18 to ^2.11.2.

    The throwing code is new in 2.11.x — FrameScope, the per-frame ctx.dispose(), and the Host.dispose() delegation all arrived upstream in fix(🐛): fix WASM memory consumption on RN Web reconciler (2026-07-13). 2.4.18 had no per-frame disposable loop.

    Deploy correlation: 9.4.89-0 (2026-09-18T19:57Z) does not contain the merge commit; 9.4.90-2 (2026-09-21T18:37Z) does. That is the Sentry first-seen date and the reported version, exactly.

    Note the patch that PR dropped — 2.4.18+003+fix-dispose-symbol-eval.patch — is a red herring despite its name: it patched native C++/JSI (cpp/jsi/JsiHostObject.cpp) and cannot affect web Safari.

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

    Define Symbol.dispose before anything can load Skia, and make it unreplaceable.

    The repo already has the right seam: src/polyfills/, imported first in index.js:4-7 above every other import. Add a SymbolDispose polyfill there, following the shape of PromiseWithResolvers.ts: if Symbol.dispose is missing, install one via Object.defineProperty. Do the same for Symbol.asyncDispose.

    Two details matter:

    1. Import it before ./src/App so it runs ahead of Skia's module evaluation. Then the class key and every call site agree for the rest of the session.
    2. Use writable: false, configurable: false, unlike the existing polyfills. A plain ??= still leaves a later vendor script free to blind-assign over the symbol and recreate the divergence. Locking it is what actually closes the bug.

    One risk worth stating: a third-party script that blind-assigns Symbol.dispose in strict mode would now throw inside its own code. core-js guards with a presence check, so it is unaffected.

    Also worth raising upstream (a one-line inversion, not required for the App fix): Host.ts should put the implementation on dispose() — a plain string key that always resolves — and have [Symbol.dispose]() call it. Same for JsiSkFontMgr.ts and JsiVideo.ts. That is immune to Symbol.dispose changing value at any time and keeps using working.

    What alternative solutions did you explore? (Optional)


    Related issues

    Confidence and what I could not verify

    Medium-high. Verified: the Safari support gap (MDN BCD), the throwing code and per-frame loop (read in node_modules), the Skia surface on /home, and the regression pinned to Expensify/App#100953 reaching production in 9.4.90 on the first-seen date.

    Not verified: which script installs Symbol.dispose late. I ruled out everything in node_modules that could reach the production bundle — core-js runtime is not installed, and @swc/helpers, tslib, canvaskit-wasm, @sentry/*, react-native-worklets and Skia itself only read the symbol, never assign it. The remaining candidates are the runtime-injected CDN scripts, whose contents I cannot inspect statically.

    I had no browser available this run, so nothing below is browser-confirmed. One console check on a Safari 18.6 machine on /home settles it: log typeof Symbol.dispose right after load and again a few seconds later. If it flips undefined → symbol, the mechanism is confirmed, and Object.getOwnPropertyNames(Object.getPrototypeOf(skiaObj)) will show a literal "undefined" key. Blocking the CDN scripts one at a time in devtools then identifies the definer.


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


    view run

  7. cretadn22 commented on Sep 28, 2026

    @cretadn22
    Contributor

    @staszekscp I noticed you opened #102138. Could you check this issue to see if your PR also resolves this one?

  8. melvin-bot commented on Sep 28, 2026

    @melvin-bot

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

  9. 15 remaining items

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

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  13. melvin-bot commented on Oct 7, 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
  14. melvin-bot commented on Oct 12, 2026

    @melvin-bot

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

  15. mallenexpensify commented on Oct 14, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @cretadn22 due $175 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