Skip to content

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

Open

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. 24 remaining items

  12. melvin-bot commented on Oct 11, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  13. 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
  14. 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

  15. Eskalifer1 commented on Oct 14, 2026

    @Eskalifer1
    Contributor

    Note about payment: I'm getting paid via Upwork

  16. melvin-bot commented on Oct 14, 2026

    @melvin-bot

    @mallenexpensify Whoops! This issue is 2 days overdue. Let's get this updated quick!

  17. mallenexpensify commented on Oct 14, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @Eskalifer1 due $100 via NewDot

    @Eskalifer1 can you please accept the job below? Please reply here and tag me once you have.
    https://www.upwork.com/jobs/~022105599600222231023

    50% cuz tests weren't needed on the PR.

  18. Eskalifer1 commented on Oct 14, 2026

    @Eskalifer1
    Contributor

    @mallenexpensify, i'm not sure payment here should be reduced, since it's Sentry issue and for most of them we don't have reliable ways to repro this, but it doesn't make them more easier in implementation and reviews

    cc: @mountiny for confirmation as CME

    BTW, i have accepted, thank you!

  19. mallenexpensify commented on Oct 15, 2026

    @mallenexpensify
    Contributor

    Looking back at the PR, it doesn't look like there were many comments, tests, screenshots of vids, so it's hard for me to confirm how much work was done. cc @mountiny

  20. Eskalifer1 commented on Oct 15, 2026

    @Eskalifer1
    Contributor

    Looking back at the PR, it doesn't look like there were many comments, tests, screenshots of vids, so it's hard for me to confirm how much work was done. cc @mountiny

    I don't think the number of comments, lines of code, or screenshots in a PR necessarily reflects the amount of work required to solve an issue.

    I'm not arguing that this was an exceptionally difficult task, but I wouldn't consider it particularly simple either. I'd classify it as a standard task that required a fair amount of investigation beyond the changes visible in the final diff.

    The main challenge was understanding the behavior of a third-party library, investigating similar issues and existing reports, and trying different approaches to reliably reproduce the problem. I also had to test the proposed solution across multiple scenarios to make sure it actually addressed the underlying issue rather than simply suppressing the error.

    A significant part of this work happened outside the PR thread. I used Melvin to help investigate potential solutions, but I also independently researched the library's behavior, looked into related open issues, experimented with reproduction steps, and manually verified the fix. Naturally, most of that investigation doesn't show up in the final diff or as comments in the PR.

    In my opinion, tasks involving third-party libraries can sometimes require more effort than seemingly similar changes within our own codebase, precisely because we have less control over the underlying implementation and often need to investigate behavior that isn't immediately obvious.

    I understand that the final implementation may look small, but I believe the amount of code required to fix a problem shouldn't be the primary indicator of the effort involved in solving it. I'd consider the investigation, debugging, and verification required here consistent with a standard-priced issue rather than an especially simple one.

  21. Eskalifer1 commented on Oct 15, 2026

    @Eskalifer1
    Contributor

    @mallenexpensify I also wanted to clarify something about the pricing policy, as this seems different from how I understood the process previously.

    Is reducing the price of an issue at the time of payment a new practice? As far as I remember, the price was generally established when an issue was created and would remain unchanged, unless there was a regression or a price increase. I don't recall issues being repriced downward after the work had already been completed. Is there any documentation or discussion where I could read more about the current policy?

    I also remember that one of the reasons for introducing the $175 base price instead of $250 was to simplify the pricing process, particularly for smaller jobs and fixes. I believe you mentioned that "we've also never had a great process for smaller jobs and fixes, so starting at $175 helps there too," and that "$175 is a floor, not a cap." My understanding was that this established a lower starting point while still allowing issues to be priced higher depending on the work involved, rather than introducing a reassessment of the price at the payment stage.

    I'm asking because I think there's an important distinction between estimating an issue's price upfront and reassessing it after the work has been completed. In the latter case, contributors may have already committed their time based on the original price, without knowing that the amount could subsequently be reduced.

    In this particular case, I worked on the issue myself, so it's not a major concern for me personally. However, I can see how this could be discouraging in a Help Wanted scenario, where a contributor accepts an issue based on its advertised price, completes the work, and then learns that the payment will be lower than expected.

    I understand that pricing may need some flexibility, especially when the actual scope turns out to be significantly smaller than anticipated. I'm mainly trying to understand whether this is an established policy, what criteria are used to determine when a downward adjustment is appropriate, and whether contributors are expected to be informed of this possibility upfront.

    I think having clear expectations around this would help keep the process transparent and fair for everyone involved, especially external contributors.

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