You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-10-12] [$175] [Sentry: APP-T6] InvalidStateError failing to start audio device #102744
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.
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
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:
Line 385: attach a no-op .catch() to self.ctx.resume().
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.
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?
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.
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).
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).
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.
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(vari=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/Failedtostarttheaudiodevice/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.
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()}elseif(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(vari=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.});}
@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!
[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.
[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
@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
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
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.
@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.
Activity
melvin-bot commentedon Oct 1, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @Eskalifer1 (
External)[-][Sentry: APP-T6] InvalidStateError failing to start audio device[/-][+][$175] [Sentry: APP-T6] InvalidStateError failing to start audio device[/+]melvin-bot commentedon Oct 1, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022105599600222231023
MelvinBot commentedon 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:touchstart,touchend,click, orkeydown(howler.core.js:385)._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:.catch()toself.ctx.resume().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)
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.html5: trueon theHowl. This doesn't fix the problem, because the global unlock handler still callsctx.resume().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:
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.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
bonnhatnguyen commentedon Oct 1, 2026
Proposal for Issue #102744: [$175] [Sentry: APP-T6] InvalidStateError failing to start audio device
Contributor Details
@bonnhatnguyenbonnhatnguyen0@gmail.comhttps://www.upwork.com/freelancers/~0139ed4098bdc50182Proposal
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 from
auto.browser.global_handlers.onunhandledrejection.What is the root cause of that problem?
/r/:reportID) are managed viahowler(v2.2.4) insrc/libs/Sound/index.ts.howler.core.js, Web Audio'sAudioContext.resume()is called in two primary places:_unlockAudio(): triggered on user gesture events (touchstart,touchend,click,keydown) to unlock audio playback._autoResume(): triggered to resume an auto-suspendedAudioContext(which Howler suspends after 30 seconds of inactivity).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).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.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):_unlockAudio:_autoResume: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:
What alternative solutions did you explore? (Optional)
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.beforeSend:Filtering only in Sentry silences the telemetry error but leaves the unhandled rejection occurring in the client browser runtime. Applying the patch in
howlercleanly handles the promise rejection at its source.Eskalifer1 commentedon Oct 1, 2026
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.🎀 👀 🎀 C+ reviewed
melvin-bot commentedon Oct 1, 2026
Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.
bonnhatnguyen commentedon Oct 1, 2026
@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!24 remaining items
melvin-bot commentedon Oct 11, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Oct 11, 2026
Payment Summary
Resolving PRs:
Upwork Job
BugZero Checklist (@mallenexpensify)
Eskalifer1 commentedon Oct 12, 2026
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
Eskalifer1 commentedon Oct 14, 2026
Note about payment: I'm getting paid via Upwork
melvin-bot commentedon Oct 14, 2026
@mallenexpensify Whoops! This issue is 2 days overdue. Let's get this updated quick!
mallenexpensify commentedon Oct 14, 2026
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.
Eskalifer1 commentedon Oct 14, 2026
@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!
mallenexpensify commentedon Oct 15, 2026
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
Eskalifer1 commentedon Oct 15, 2026
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.
Eskalifer1 commentedon Oct 15, 2026
@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.