Skip to content

fix: warn instead of rejecting when a data stream handler fails after the room disconnects - #2201

Draft
1egoman wants to merge 3 commits into
mainfrom
fix/catch-stream-handler-errors
Draft

1egoman wants to merge 3 commits into
mainfrom
fix/catch-stream-handler-errors

Conversation

@1egoman

@1egoman 1egoman commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

I'm not convinced this is a good idea, but I think this could be one way to address a limited subset of the issue from livekit/components-js#1426.

Summary

IncomingDataStreamManager calls registered text/byte stream handlers without awaiting them. Most handlers are async functions that read the stream with no try/catch, so any error they don't catch becomes an unhandled promise rejection.

That is mostly the right default: a lost chunk (Incomplete), a sender dropping mid stream, or a bug in the handler should fail loudly so apps notice. The exception is the local room disconnecting. Since #2125, clearControllers() errors every in-flight stream with Room disconnected in the middle of receiving data, so hanging up while (for example) an agent transcription is still streaming produces an unhandled rejection that the app can't do anything about. The session is over at that point.

Downstream report: livekit/components-js#1426.

Fix

Handlers are now invoked through invokeStreamHandler, which looks at a handler's rejection:

  • Room has disconnected → log a warning (topic, stream id, error) instead of rejecting.
  • Otherwise → re-throw it unchanged, so it surfaces as an unhandled rejection exactly as before.

Whether the room has disconnected is tracked by a new hasDisconnected flag. clearControllers() sets it (it only runs from handleDisconnect), and setConnected(true) clears it. The existing isConnected flag isn't used because it's also false while reconnecting, and errors during a reconnect the room recovers from should stay loud.

The check happens when the handler's error arrives, not when the stream started. Any handler error that lands after the disconnect is downgraded, even one unrelated to the disconnect (an app bug, or a lost chunk just before hangup). That's intended: once the room is closed, failures are out of the app's hands, and they're still logged.

TextStreamHandler/ByteStreamHandler are widened to return void | Promise<void> to document that async handlers are supported. This doesn't break existing handlers.

Unchanged (still unhandled rejections)

  • A remote participant disconnecting mid stream (AbnormalEnd from validateParticipantHasNoActiveDataStreams)
  • A lost chunk (Incomplete)
  • Any handler error while connected or reconnecting
  • Synchronous throws from a handler (these can only happen while connected, since packets are buffered until connected)

Known edge case

If an app disconnects and reconnects the same Room before a slow handler from the previous session fails, that error arrives after setConnected(true) has cleared the flag and stays loud. That seemed rare enough not to be worth tracking per session.

Tests

New Stream handler errors block in IncomingDataStreamManager.test.ts:

  • local disconnect cutting off text and byte streams → warning, no rejection
  • unrelated handler error landing after the disconnect → warning, no rejection
  • remote disconnect mid stream → still an unhandled rejection
  • lost chunk → still an unhandled rejection (Incomplete)
  • handler error while reconnecting → still an unhandled rejection
  • handler error after disconnect then reconnect → unhandled rejection again

The first two fail on main, and the other four pass on main (they check that existing behaviour is kept). Full suite, tsc, eslint and prettier pass.

🤖 Generated with Claude Code

Registered text/byte stream handlers were invoked without awaiting or
catching their result. An async handler that iterated its reader without
a try/catch therefore produced an unhandled promise rejection whenever the
stream ended abnormally (sender or local room disconnecting mid stream),
and a synchronous throw escaped out of packet handling.

Invoke handlers through a wrapper that catches both: AbnormalEnd
DataStreamErrors are logged at debug, anything else at error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d4f2950

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
livekit-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
dist/livekit-client.esm.mjs 116.19 KB (+0.05% 🔺)
dist/livekit-client.umd.js 125.52 KB (+0.02% 🔺)
dist/core/core.mjs 78.83 KB (0%)

@1egoman 1egoman closed this Oct 27, 2026
…ects

Rather than catching every handler error, re-throw anything that happens
while the room is connected (or reconnecting) so it still surfaces as an
unhandled rejection, e.g. a lost chunk or a remote participant leaving
mid stream. Only errors that land after the room has disconnected are
logged as a warning, since the session is over and the failure is out of
the application's hands.

A dedicated flag, set by clearControllers() and cleared by
setConnected(true), tracks the disconnect so a reconnect isn't mistaken
for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1egoman 1egoman reopened this Oct 27, 2026
@1egoman 1egoman changed the title fix: catch errors thrown by data stream handlers fix: warn instead of rejecting when a data stream handler fails after the room disconnects Oct 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant