Skip to content

fix(client): forward async room handler rejections to onError - #994

Open
SaifShafi wants to merge 1 commit into
elevenlabs:mainfrom
SaifShafi:fix/forward-async-handler-errors
Open

SaifShafi wants to merge 1 commit into
elevenlabs:mainfrom
SaifShafi:fix/forward-async-handler-errors

Conversation

@SaifShafi

@SaifShafi SaifShafi commented Sep 2, 2026

Copy link
Copy Markdown

Closes #803, for the WebRTCConnection half.

The bug

Two LiveKit room handlers in WebRTCConnection are async, and an emitter discards the promise a handler returns. A rejection therefore became an unhandled rejection rather than reaching the caller.

TrackSubscribed is the one that matters. It awaits audioAdapter.attachRemoteTrack and setupAudioCapture:

this.room.on(RoomEvent.TrackSubscribed, async (track, _publication, participant) => {
  ...
  await this.audioAdapter.attachRemoteTrack(remoteAudioTrack, this.outputDeviceId);
  await this.setupAudioCapture(remoteAudioTrack);

If either fails, the agent's audio never attaches and the consumer is told nothing at all. No error, no disconnect, just silence, which is about the worst shape a failure can take in a voice SDK.

The fix

onError alongside the existing onDebug on BaseConnection, plus a forwardHandlerErrors helper that catches a handler's rejection and reports it. Applied to TrackSubscribed and ActiveSpeakersChanged.

onError is declared on WebRTCConnectionConfig next to onDebug, so the contract is explicit rather than relying on the structural pass-through that currently gets onDebug to the connection.

The helper lives on BaseConnection rather than in WebRTCConnection because #803 says "and potentially others", so the next one is a one-line wrap.

Scope

Only WebRTCConnection. The issue also names scribe.ts streamFromMicrophone, but that already catches internally, calls connection._emitError(error) and closes the connection, so it needs no change. Worth confirming if you had something else in mind there.

Verification

Four tests added. To check they actually detect the bug rather than just passing, I reverted the helper to the previous fire-and-forget behaviour and reran:

test pre-fix post-fix
forwards a TrackSubscribed rejection to onError fail pass
does not leave the rejection unhandled fail pass
forwards an ActiveSpeakersChanged rejection to onError fail pass
stays silent when the handler resolves pass pass

The last is the negative control and passes either way, by design.

The unhandled-rejection test subscribes to process.on("unhandledRejection") and asserts nothing is emitted, which is the specific symptom the issue describes.

Full client package: prettier, eslint and tsc --noEmit clean, 230 unit tests passing.

Patch changeset included.

Two LiveKit room handlers in WebRTCConnection are async, and an emitter
discards the promise a handler returns. A rejection therefore became an
unhandled rejection rather than reaching the caller.

TrackSubscribed is the one that matters: it awaits
audioAdapter.attachRemoteTrack and setupAudioCapture, so if either fails the
agent's audio never attaches, and the consumer is told nothing at all. There
is no error, no disconnect, just silence.

Adds onError alongside the existing onDebug on BaseConnection, and a
forwardHandlerErrors helper that catches a handler's rejection and reports it.
Applies it to TrackSubscribed and ActiveSpeakersChanged. onError is declared
on WebRTCConnectionConfig next to onDebug so the contract is explicit rather
than relying on structural pass-through.

Scoped to WebRTCConnection. The issue also lists scribe.ts
streamFromMicrophone, but that already catches internally, calls
connection._emitError and closes, so it needs no change.

Three of the four added tests fail against the previous behaviour, including
one asserting no unhandled rejection is emitted. The fourth is the negative
control and passes either way.

Refs elevenlabs#803
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR Summary

Low Risk
Additive optional callback and defensive error routing on the WebRTC event path; happy-path behavior is unchanged aside from caught failures being reported instead of going unhandled.

Overview
Fixes silent WebRTC failures when async LiveKit room handlers reject: LiveKit ignores returned promises, so errors in TrackSubscribed (agent audio attach / capture setup) or ActiveSpeakersChanged used to become unhandled rejections with no app-visible signal.

Adds an optional onError(message, context?) on BaseConnection / WebRTCConnection, plus forwardHandlerErrors to .catch() those async handlers and call it with fixed messages (e.g. "Failed to attach subscribed audio track"). New tests assert forwarding, no unhandled rejections, and no onError on success.

Reviewed by Cursor Bugbot for commit eab4441. Bugbot is set up for automated code reviews on this repo. Configure here.

@SaifShafi SaifShafi changed the title probe fix(client): forward async room handler rejections to onError Sep 2, 2026
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.

Forward unhandled async errors through onError callbacks

1 participant