Skip to content

fix: handle rejected updateInputSettings so Krisp failures emit 'error' - #170

Merged
amanda-vapi merged 1 commit into
mainfrom
claude/krisp-unhandled-rejection
Aug 14, 2026
Merged

fix: handle rejected updateInputSettings so Krisp failures emit 'error'#170
amanda-vapi merged 1 commit into
mainfrom
claude/krisp-unhandled-rejection

Conversation

@claude

@claude claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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 synchronous try/catch. The call is never awaited and has no .catch(). A synchronous catch cannot 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 — the catch block 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, all handled=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 in start() and in reconnect().

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 dead catch block intended (type: 'audio-processing-setup-error'), and the recovery path reports type: '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 surrounding try/catch blocks 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. In reconnect() this is new (its catch block 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 — EventEmitter throws ERR_UNHANDLED_ERROR on an unlistened 'error' — exactly as it got an unhandled Canceled before. Guarding on listenerCount('error') would silently drop the failure and would differ from the ten other emit('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:

  • The recovery path's event-type gate is dead on the production Daily pin. vapi.ts:822 / vapi.ts:1346 only run when the nonfatal-error has type === 'audio-processor-error', but @daily-co/daily-js 0.91.0 emits type: '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-progress reports false success. When updateInputSettings() rejects, the event still reports audio-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.ts drive the real start() and reconnect() with a Daily call object whose updateInputSettings() rejects with Error('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 unmodified vapi.ts all 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 build and tsc --noEmit are clean.

Note on the "no unhandled rejection" half: Jest runs test code in a vm sandbox that never delivers process-level unhandledRejection events, so a process.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.1 yields 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.yml the package.json / package-lock.json bump belongs to the release PR.


Generated by Claude Code

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
gregory-vapi marked this pull request as ready for review August 14, 2026 19:27
@amanda-vapi
amanda-vapi merged commit 939b4ac into main Aug 14, 2026
6 checks passed
@amanda-vapi
amanda-vapi deleted the claude/krisp-unhandled-rejection branch August 14, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants