Repository navigation
[Due for payment 2026-10-12] [$175] [Sentry: APP-T6] InvalidStateError failing to start audio device #102744
Description
Activity
- addedExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorDailyKSv2KSv2BugSomething is broken. Auto assigns a BugZero manager.Something is broken. Auto assigns a BugZero manager.
on Oct 1, 2026 Triggered auto assignment to Contributor-plus team member for initial proposal review - @Eskalifer1 (
External)- 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 Job added to Upwork: https://www.upwork.com/jobs/~022105599600222231023
- added a parent issue
on Oct 1, 2026 Proposal
What is the root cause of that problem?
The Web build plays sounds with
howler2.2.4 (src/libs/Sound/index.ts:34-46). Howler callsAudioContext.resume()in two places and never handles a rejected promise:- In the unlock handler that runs on every first
touchstart,touchend,click, orkeydown(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()withInvalidStateError: Failed to start the audio devicewhen 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 ouronplayerrorcallback 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'ssrc.What changes do you think we should make in order to solve the problem?
Add a
patch-packagepatch forhowler2.2.4 underpatches/howler/, with adetails.mdlike the other patches:- Line 385: attach a no-op
.catch()toself.ctx.resume(). - Line 525: add a rejection handler to
self.ctx.resume().then(...). Leavestateas'suspended'so the nextplaySoundcall 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.resumeinSound/index.tsand swallow rejections there. This avoids a patch, but it monkey-patches a library global and must run after Howler creates the context. - Use
html5: trueon theHowl. This doesn't fix the problem, because the global unlock handler still callsctx.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.tsuses Web Audio (AudioContext,Audio, orHowl) insrc. Native usesexpo-audioinindex.native.ts. howler2.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.
- In the unlock handler that runs on every first
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-T6records recurring unhandled promise rejections on Web (primarily iOS Safari / standalone PWA) with:
InvalidStateError: Failed to start the audio device (DOMException code 11)
originating fromauto.browser.global_handlers.onunhandledrejection.
What is the root cause of that problem?
- On Web platforms, sound effects (such as message send/receive feedback on report routes
/r/:reportID) are managed viahowler(v2.2.4) insrc/libs/Sound/index.ts. - Inside
howler.core.js, Web Audio'sAudioContext.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-suspendedAudioContext(which Howler suspends after 30 seconds of inactivity).
- In
- On iOS WebKit / Safari (especially when the app runs as a standalone PWA),
AudioContext.resume()returns a Promise that rejects withInvalidStateError: Failed to start the audio deviceif 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). - Because
howler2.2.4 does not attach a rejection handler (.catch(...)) to theseself.ctx.resume()promises, the rejection escapes as an unhandled promise rejection towindow.onunhandledrejection. - Additionally,
src/setup/telemetry/setupSentry.tsdoes not include this known benign browser DOMException in itsignoreErrorsarray.
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
howlerpatch viapatch-package(patches/howler/howler+2.2.4.patch):In
dist/howler.js(andsrc/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.mdexplaining the patch purpose.
2. Add regex filter to
ignoreErrorsinsrc/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.resumeinsrc/libs/Sound/index.ts:
While possible in userland,Howler.ctxis instantiated lazily inside Howler, making module load-order sensitive and less clean than the standardpatch-packageworkflow 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 inhowlercleanly handles the promise rejection at its source.
- 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 setsstate = 'running'inside.then(), afterresume()has succeeded. Ifresume()is rejected,Howler.statestays'suspended', so the nextplay()goes through_autoResume()and callsresume()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
Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.
@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!15 remaining items
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]
Turning this into weekly since it was deployed to prod
@MelvinBot find offending PR
Reacted by melvin-botOffending PR: feat: app sounds (
Expensify/App#31055), merged 2024-02-12.This PR added Web sound playback by aliasing
react-native-soundtoreact-native-web-sound, which pulled inhowler2.2.4. Howler callsAudioContext.resume()without a rejection handler (howler.core.js:385and: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 resolvesreact-native-web-sound0.1.3 →howler2.2.4. - Resolves offline sound playback by caching assets locally (merged 2024-10-21) later replaced
react-native-web-soundwith a directhowlerimport. It kept the samehowler2.2.4, so it didn't change this behavior. - Neither
howler2.2.3 nor 2.2.4 handles theresume()rejection at either call site. No App commit touchedsrc/libs/Sound/index.tsbetween 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.
- #31055 adds
- addedDailyKSv2KSv2Awaiting PaymentAuto-added when associated PR is deployed to productionAuto-added when associated PR is deployed to productionand removedWeeklyKSv2KSv2
on Oct 11, 2026 Triggered auto assignment to @mallenexpensify (
Awaiting Payment)Payment Summary
Resolving PRs:
- 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
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
-
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Sentry
https://expensify.sentry.io/issues/APP-T6
Impact (snapshot at filing)
Stack trace (top frames, first-party only)
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
Upwork Automation - Do Not Edit
Issue Owner
Current Issue Owner: @mallenexpensify