fix: handle rejected updateInputSettings so Krisp failures emit 'error' - #170
Merged
Conversation
Enabling noise cancellation calls `this.call.updateInputSettings(...)` inside a synchronous try/catch, without awaiting it and without a rejection handler. A synchronous catch cannot observe a promise rejection, so when Daily's remotely loaded call machine rejects - `KrispInitError: Canceled`, seen since Daily SDK 0.91.0 landed on 2026-08-07 - the catch block never runs. Its emission of the SDK's own 'error' event never happens either, and the rejection escapes to the page as an unhandled rejection instead of reaching the listeners consumers register. dashboard.vapi.ai has taken 101 of these across 93 users, all handled=false, still firing about 14 a day. Attach a rejection handler at all four sites - the noise-cancellation setup and the 'audio-processor-error' recovery chain, each duplicated in start() and in reconnect() - routing the failure through one new private helper, emitAudioProcessingError(), which emits the same 'error' payload the dead catch block intended. Audio processing stays non-critical: the call is unaffected, the failure is now observable. Covered by four new cases in __tests__/vapi.test.ts that drive the real start() and reconnect() paths with an updateInputSettings() that rejects. They fail against unmodified vapi.ts (no rejection handler is attached, no 'error' event is emitted) and pass with this change. Fixes VAP-15826
gregory-vapi
approved these changes
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Gregory Beaver · Slack thread
Fixes VAP-15826
What it does: makes a failed Krisp/noise-cancellation setup emit the SDK's
'error'event instead of escaping to the page as an unhandled promise rejection.Before
Starting a call enables noise cancellation with
this.call.updateInputSettings({ audio: { processor: { type: 'noise-cancellation' } } }), wrapped in a synchronoustry/catch. The call is never awaited and has no.catch(). A synchronouscatchcannot see a promise rejection, so when Daily's remotely loaded call machine rejects —KrispInitError: Canceled, appearing since Daily SDK 0.91.0 landed on 2026-08-07 — thecatchblock never runs. Nothing the block was written to do happens, including emitting the SDK's own'error'event, which consumers such as the Vapi dashboard do handle. The rejection surfaces in the page instead: 101 unhandled rejections across 93 distinct users on dashboard.vapi.ai, allhandled=false, still arriving at roughly 14 a day.The same defect exists at four places: the noise-cancellation setup and the
'audio-processor-error'recovery (which only chains.then()), each duplicated instart()and inreconnect().After
Every one of those four
updateInputSettings()calls has a rejection handler. A failure is reported on the'error'event with the same payload the deadcatchblock intended (type: 'audio-processing-setup-error'), and the recovery path reportstype: 'audio-processor-recovery-error'. Nothing reaches the page unhandled, and the failure stays non-critical: the call starts, and reconnect still resolves, exactly as before.How
One new private helper,
emitAudioProcessingError(stage, error), defined above its first use, serializes the error and emits'error'. Each of the four sites gains a.catch()that calls it. The surroundingtry/catchblocks are left in place — they still guard a synchronous throw — and no existing event, ordering, or timing changes:updateInputSettings()is still not awaited, so call start is not delayed by Krisp loading. The only behavioural difference is the added'error'emission, which is what was intended all along. Inreconnect()this is new (itscatchblock never emitted'error'), so both paths now report identically.One limit worth stating: this routes the failure to
'error'listeners, so a consumer that registers none still ends up with an unhandled rejection —EventEmitterthrowsERR_UNHANDLED_ERRORon an unlistened'error'— exactly as it got an unhandledCanceledbefore. Guarding onlistenerCount('error')would silently drop the failure and would differ from the ten otheremit('error')sites in this file, so it is left alone.Known follow-ups (not fixed here)
Two things a live reproduction on 2026-08-14 turned up, both tracked on VAP-15826 and both deliberately out of scope for this minimal fix:
vapi.ts:822/vapi.ts:1346only run when thenonfatal-errorhastype === 'audio-processor-error', but@daily-co/daily-js0.91.0 emitstype: 'input-settings-error'for this failure. So the two.catch()handlers added to the recovery calls here are correct but inert until the gate is fixed — a separate defect from the un-awaited promise.call-start-progressreports false success. WhenupdateInputSettings()rejects, the event still reportsaudio-processing-setup: completed. This PR makes the failure observable on'error'; it does not change the progress event, so consumers of that event still see a clean start.Tests
Four new cases in
__tests__/vapi.test.tsdrive the realstart()andreconnect()with a Daily call object whoseupdateInputSettings()rejects withError('Canceled'), and assert (a) the SDK attaches a rejection handler to the promise it gets back, so nothing escapes, and (b) the matching'error'event carries the rejection. Against unmodifiedvapi.tsall four fail, with the stack traces pointing at the four sites (vapi.ts:845,822,1516,1346); with the fix all four pass, and the 6 existing cases still pass.npm run buildandtsc --noEmitare clean.Note on the "no unhandled rejection" half: Jest runs test code in a vm sandbox that never delivers process-level
unhandledRejectionevents, so aprocess.on('unhandledRejection')assertion would be green no matter what the SDK does. The tests assert handler attachment at the source instead. The escape itself was reproduced outside Jest against the published bundle: unpatched@vapi-ai/web@2.6.1yields 2 unhandled rejections and 0'error'events, and with this fix 0 unhandled rejections and 2'error'events.No version bump here — per
.github/workflows/release.ymlthepackage.json/package-lock.jsonbump belongs to the release PR.Generated by Claude Code