Skip to content

[Due for payment 2026-10-12] [$175] [Sentry: APP-T6] InvalidStateError failing to start audio device #102744

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-T6

Impact (snapshot at filing)

  • Users (total since first seen): 119
  • Events: 389
  • Users (last 14d): not pulled separately
  • First seen: 2025-11-07 (~11 months ago, still ongoing)
  • Last seen: now (ongoing)
  • Platform: web (mobile Safari, standalone PWA)
  • App version(s): seen across many releases through current production (9.4.99)
  • Affected route(s) (parameterized): /r/:reportID
  • Mechanism: auto.browser.global_handlers.onunhandledrejection

Stack trace (top frames, first-party only)

No stacktrace available — browser does not provide one for this DOMException

Error: InvalidStateError: Failed to start the audio device (DOMException code 11)

Suspected cause

Mobile Safari throws this when starting the audio device fails, likely during voice message recording or playback initialization on a report page. No stack is available because the browser suppresses it for this API.

Reproduction

Unknown — see Sentry events linked from the Sentry issue.

Related

  • Sibling Sentry IDs: none identified
  • Prior GH issues: none found
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

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

    @melvin-bot

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

  4. changed the title [-][Sentry: APP-T6] InvalidStateError failing to start audio device[/-] [+][$175] [Sentry: APP-T6] InvalidStateError failing to start audio device[/+] on Oct 1, 2026
  5. melvin-bot commented on Oct 1, 2026

    @melvin-bot
  6. MelvinBot commented on Oct 1, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The Web build plays sounds with howler 2.2.4 (src/libs/Sound/index.ts:34-46). Howler calls AudioContext.resume() in two places and never handles a rejected promise:

    • In the unlock handler that runs on every first touchstart, touchend, click, or keydown (howler.core.js:385).
    • In _autoResume, which runs when a sound plays after the context was auto-suspended. Howler suspends the context after 30 seconds of silence (howler.core.js:525).

    iOS Safari rejects resume() with InvalidStateError: Failed to start the audio device when it can't start the audio session. For example, this happens when another app or a phone call holds the audio session, or the PWA was just backgrounded. The rejection happens inside Howler, so our onplayerror callback never sees it. Sentry reports it as an unhandled rejection with no stack trace.

    This matches the Sentry data: iOS standalone PWA only, on /r/:reportID, where sounds play on send and receive. Howler is the only Web Audio user in App's src.

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

    Add a patch-package patch for howler 2.2.4 under patches/howler/, with a details.md like the other patches:

    1. Line 385: attach a no-op .catch() to self.ctx.resume().
    2. Line 525: add a rejection handler to self.ctx.resume().then(...). Leave state as 'suspended' so the next playSound call tries again.

    Patch dist/howler.js, the file the bundler resolves ("main"). If a resume fails, that one sound stays silent, which is already what happens today. Only the Sentry noise goes away.

    What alternative solutions did you explore? (Optional)

    • Wrap Howler.ctx.resume in Sound/index.ts and swallow rejections there. This avoids a patch, but it monkey-patches a library global and must run after Howler creates the context.
    • Use html5: true on the Howl. This doesn't fix the problem, because the global unlock handler still calls ctx.resume().
    • Filter the error in Sentry beforeSend. This hides the symptom but leaves the unhandled rejection in place.
    Confidence and evidence

    Medium confidence. The browser gives no stack trace, so the link to Howler is inferred, not proven. Evidence:

    • Only src/libs/Sound/index.ts uses Web Audio (AudioContext, Audio, or Howl) in src. Native uses expo-audio in index.native.ts.
    • howler 2.2.4 is the latest release, so there is no upstream fix to upgrade to.
    • I didn't reproduce it, because it needs an iOS PWA whose audio session is busy. To verify, open the PWA on iOS during a call or while another app plays audio, then tap in a report or send a message. Check for an unhandled rejection in the Safari Web Inspector console.

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


    view run

  7. bonnhatnguyen commented on Oct 1, 2026

    @bonnhatnguyen

    Proposal for Issue #102744: [$175] [Sentry: APP-T6] InvalidStateError failing to start audio device

    Contributor Details

    • Contributor: @bonnhatnguyen
    • Email: bonnhatnguyen0@gmail.com
    • Upwork: https://www.upwork.com/freelancers/~0139ed4098bdc50182

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Sentry issue APP-T6 records recurring unhandled promise rejections on Web (primarily iOS Safari / standalone PWA) with:
    InvalidStateError: Failed to start the audio device (DOMException code 11)
    originating from auto.browser.global_handlers.onunhandledrejection.


    What is the root cause of that problem?

    1. On Web platforms, sound effects (such as message send/receive feedback on report routes /r/:reportID) are managed via howler (v2.2.4) in src/libs/Sound/index.ts.
    2. Inside howler.core.js, Web Audio's AudioContext.resume() is called in two primary places:
      • In _unlockAudio(): triggered on user gesture events (touchstart, touchend, click, keydown) to unlock audio playback.
      • In _autoResume(): triggered to resume an auto-suspended AudioContext (which Howler suspends after 30 seconds of inactivity).
    3. On iOS WebKit / Safari (especially when the app runs as a standalone PWA), AudioContext.resume() returns a Promise that rejects with InvalidStateError: Failed to start the audio device if the device's audio session cannot be activated at that moment (for example, if another app or telephony process holds exclusive audio focus, or if the PWA was backgrounded/restored).
    4. Because howler 2.2.4 does not attach a rejection handler (.catch(...)) to these self.ctx.resume() promises, the rejection escapes as an unhandled promise rejection to window.onunhandledrejection.
    5. Additionally, src/setup/telemetry/setupSentry.ts does not include this known benign browser DOMException in its ignoreErrors array.

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

    We should apply a two-layer defense adhering to Expensify standards:

    1. Add howler patch via patch-package (patches/howler/howler+2.2.4.patch):

    In dist/howler.js (and src/howler.core.js):

    • In _unlockAudio:
      self.ctx.resume().then(function() {
          // unlock logic
      }).catch(function() {
          // Audio device failed to start; safely swallow rejection so it does not trigger an unhandledrejection
      });
    • In _autoResume:
      self.ctx.resume().then(function() {
          self.state = 'running';
          self._emit('resume');
          for (var i = 0; i < self._queue.length; i++) {
              if (self._queue[i].event === 'resume') {
                  self._queue[i].fn();
                  self._queue.splice(i, 1);
                  i--;
              }
          }
      }).catch(function() {
          // Keep state as 'suspended' so future audio calls gracefully attempt to resume
      });
    • Add documentation in patches/howler/details.md explaining the patch purpose.

    2. Add regex filter to ignoreErrors in src/setup/telemetry/setupSentry.ts:

    Add a filter to prevent any external or unpatched WebKit audio device rejections from polluting Sentry logs:

    ignoreErrors: [
        ...
        // Harmless Safari/PWA audio session acquisition failure when another app/call holds audio focus
        /Failed to start the audio device/i,
    ],

    What alternative solutions did you explore? (Optional)

    • Monkey-patching Howler.ctx.resume in src/libs/Sound/index.ts:
      While possible in userland, Howler.ctx is instantiated lazily inside Howler, making module load-order sensitive and less clean than the standard patch-package workflow already used widely in Expensify.
    • Sentry-only filtering in beforeSend:
      Filtering only in Sentry silences the telemetry error but leaves the unhandled rejection occurring in the client browser runtime. Applying the patch in howler cleanly handles the promise rejection at its source.
  8. Eskalifer1 commented on Oct 1, 2026

    @Eskalifer1
    Contributor

    I think we can move forward with Melvin here. I wasn't able to reproduce the bug myself since I don't have an iOS device(Iphone or IPad)(wasn't able to repro on PC), but it seems there could be many causes for this error. -for example, trying to play a sound while on a phone call, microphone recording, and so on. The bug only occurs in Safari, mostly on mobile, but sometimes on a PC as well.

    I believe Melvin’s proposal is correct, since this problem was already raised last year in this issue: goldfire/howler.js#1743

    And a PR to fix this has been open for over a year now. It seems like adding a no-op .catch() is enough, because howler only sets state = 'running' inside .then(), after resume() has succeeded. If resume() is rejected, Howler.state stays 'suspended', so the next play() goes through _autoResume() and calls resume() again. The .catch() only stops the rejection from surfacing as an unhandled promise rejection; it doesn't change howler's retry logic.

    // howler/dist/howler.js — Howler._autoResume()
    } else if (self.state === 'suspended' || self.state === 'running' && self.ctx.state === 'interrupted') {
      self.ctx.resume().then(function() {
        self.state = 'running'; // only reached on success
    
        // Emit to all Howls that the audio has resumed.
        for (var i=0; i<self._howls.length; i++) {
          self._howls[i]._emit('resume');
        }
      }).catch(function() {
        // The browser refused to start the audio device. Keep the state as is so the next play() tries to resume again.
      });
    }

    🎀 👀 🎀 C+ reviewed

  9. melvin-bot commented on Oct 1, 2026

    @melvin-bot

    Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

  10. bonnhatnguyen commented on Oct 1, 2026

    @bonnhatnguyen

    @Eskalifer1 @mountiny I have this exact patch, details.md, and defensive telemetry filter prepared as outlined in my proposal above, and I'm ready to submit the PR right away!

  11. 15 remaining items

  12. melvin-bot commented on Oct 5, 2026

    @melvin-bot

    @Eskalifer1

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.2-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-12. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  13. Eskalifer1 commented on Oct 6, 2026

    @Eskalifer1
    Contributor

    Turning this into weekly since it was deployed to prod

  14. Eskalifer1 commented on Oct 9, 2026

    @Eskalifer1
    Contributor

    @MelvinBot find offending PR

  15. MelvinBot commented on Oct 9, 2026

    @MelvinBot
    Contributor

    Offending PR: feat: app sounds (Expensify/App#31055), merged 2024-02-12.

    This PR added Web sound playback by aliasing react-native-sound to react-native-web-sound, which pulled in howler 2.2.4. Howler calls AudioContext.resume() without a rejection handler (howler.core.js:385 and :525), so the Safari rejection has been unhandled since this PR shipped.

    The Sentry first-seen date of 2025-11-07 is not a regression. It reflects when Sentry started reporting from production, after [NO QA] feat: setup Sentry as a observability solution merged on 2025-10-28.

    Evidence
    • #31055 adds 'react-native-sound': 'react-native-web-sound' to the webpack aliases. Its lockfile resolves react-native-web-sound 0.1.3 → howler 2.2.4.
    • Resolves offline sound playback by caching assets locally (merged 2024-10-21) later replaced react-native-web-sound with a direct howler import. It kept the same howler 2.2.4, so it didn't change this behavior.
    • Neither howler 2.2.3 nor 2.2.4 handles the resume() rejection at either call site. No App commit touched src/libs/Sound/index.ts between 2025-06 and 2026-01, so nothing in App changed near the Sentry first-seen date.
    • The root cause is a missing .catch() inside a third-party library. A reviewer could not have caught it from the App diff, so this isn't a reviewer miss.

    view run

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

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  18. melvin-bot commented on Oct 11, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    Upwork Job

    • Assignee: @Eskalifer1 requires payment (Needs manual offer from BZ)

    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
  19. Eskalifer1 commented on Oct 12, 2026

    @Eskalifer1
    Contributor

    Contributor+ Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to the comment on the PR: https://github.com/Expensify/App/pull/31055/changes#r4245268159

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion: N/A

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

      Link to discussion: N/A - no reliable steps to reproduce

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