Description
livekit-client@2.17.2 running under React Native + Hermes (Android) reports a recurring TypeError: Cannot read property 'Closing' of undefined that originates fully inside livekit-client.umd.js. The throw happens in the finally block of RTCEngine.negotiate() when it tries to remove its own listeners after the negotiate body rejects.
The TypeError is captured by Sentry's onunhandledrejection handler — the app doesn't crash, but the underlying negotiation has already failed and the cleanup itself now also throws, masking the original error.
We are seeing this across 258 unique users / 715 events over ~2 months in production. It first appeared in our 1.3.0 release (mid-March 2026) and is still firing in 1.3.6. Steady drip, not a single bad release — strongly suggesting a runtime/device-specific UMD-on-Hermes interaction.
Environment
livekit-client: 2.17.2 (livekit-client.umd.js build)
@livekit/react-native: ^2.9.5
@livekit/react-native-webrtc: ^137.0.2
- React Native + Hermes (Android), Expo SDK
- Affected devices observed: low/mid-tier Android tablets and phones (e.g. Lenovo TB-9707F + Android 13)
- iOS: not observed
Stack trace (minified UMD + symbolicated)
Top frames:
TypeError: Cannot read property 'Closing' of undefined
at anonymous (app:///index.android.bundle:1:1875)
at tryCallOne (app:///index.android.bundle:1:1181)
at a (livekit-client.umd.js:1:84844) <-- generic awaiter helper "a"
at throw (native)
at Qi$argument_3 (livekit-client.umd.js:1:299372) <-- RTCEngine.negotiate TypedPromise executor
Minified context at the throw site (frame 4):
}finally{this.off(e.EngineEvent.Closing,s),this.off(e.EngineEvent.Restarting,s)}
Source-mapped to src/room/RTCEngine.ts @ v2.17.2, lines 1562-1565:
} finally {
this.off(EngineEvent.Closing, handleClosed);
this.off(EngineEvent.Restarting, handleClosed);
}
Where e is the bundled module-namespace object (UMD output for import * as e from './events') and EngineEvent is the TS enum exported from that module.
Sentry metadata
mechanism.type: onunhandledrejection
mechanism.handled: true
tags.hermes: true
tags.os: Android 13
- All 715 events match this exact 5-frame stack — same minified
Qi$argument_3 site every time.
What appears to be happening
For e.EngineEvent.Closing to throw "Cannot read property 'Closing' of undefined" the e.EngineEvent access itself returns undefined, then .Closing is read off that.
We don't believe the entire UMD namespace e is undefined — RTCEngine.negotiate() would have failed much earlier in that case. Our best guess: the EngineEvent enum on the namespace e is missing at this point in execution. Possibilities:
- Hermes module-namespace lazy proxy — Hermes's UMD-as-module shim materializes the namespace lazily and an enum re-export drops in some edge case.
- Race / TDZ on tree-shaken re-export — the enum is re-exported from
src/room/events.ts and the UMD output threads it through several variable assignments; on certain reject timings the binding isn't yet visible to the catch/finally closure.
- Sentry React Native SDK instrumentation — Sentry wraps RN's
Promise to capture unhandled rejections; the wrapper might run cleanup against a partially-deinitialized closure scope on a hot-reload or bundle-eviction path.
We can't reproduce on demand — it's intermittent, low-spec-Android only, and only fires when negotiate() rejects (e.g. ICE candidate exhaustion, signal disconnect mid-negotiate).
Related upstream history
Workaround applied on our side
Sentry beforeSend filter that drops events matching the exact 4-clause signature (TypeError + exact message + livekit-client.umd.js frame + onunhandledrejection mechanism). This stops the noise in our Sentry but doesn't fix anything for users — the negotiation reconnect path is still silently broken whenever this fires.
Ask
- Confirmation whether this is a known UMD-on-Hermes pattern.
- If yes, can the
negotiate() finally-block defensively guard against EngineEvent being undefined (e.g. capture EngineEvent.Closing / EngineEvent.Restarting into local consts at function entry, before the await)? That would make the finally block robust regardless of namespace resolution timing.
- Tracking issue we can subscribe to so we know when to remove our beforeSend filter.
Happy to provide more event data, runtime tags, or a sanitized stack export if useful.
Description
livekit-client@2.17.2running under React Native + Hermes (Android) reports a recurringTypeError: Cannot read property 'Closing' of undefinedthat originates fully insidelivekit-client.umd.js. The throw happens in thefinallyblock ofRTCEngine.negotiate()when it tries to remove its own listeners after the negotiate body rejects.The TypeError is captured by Sentry's
onunhandledrejectionhandler — the app doesn't crash, but the underlying negotiation has already failed and the cleanup itself now also throws, masking the original error.We are seeing this across 258 unique users / 715 events over ~2 months in production. It first appeared in our 1.3.0 release (mid-March 2026) and is still firing in 1.3.6. Steady drip, not a single bad release — strongly suggesting a runtime/device-specific UMD-on-Hermes interaction.
Environment
livekit-client: 2.17.2 (livekit-client.umd.jsbuild)@livekit/react-native:^2.9.5@livekit/react-native-webrtc:^137.0.2Stack trace (minified UMD + symbolicated)
Top frames:
Minified context at the throw site (frame 4):
Source-mapped to
src/room/RTCEngine.ts@ v2.17.2, lines 1562-1565:Where
eis the bundled module-namespace object (UMD output forimport * as e from './events') andEngineEventis the TS enum exported from that module.Sentry metadata
mechanism.type:onunhandledrejectionmechanism.handled:truetags.hermes:truetags.os: Android 13Qi$argument_3site every time.What appears to be happening
For
e.EngineEvent.Closingto throw "Cannot read property 'Closing' of undefined" thee.EngineEventaccess itself returnsundefined, then.Closingis read off that.We don't believe the entire UMD namespace
eis undefined —RTCEngine.negotiate()would have failed much earlier in that case. Our best guess: theEngineEventenum on the namespaceeis missing at this point in execution. Possibilities:src/room/events.tsand the UMD output threads it through several variable assignments; on certain reject timings the binding isn't yet visible to the catch/finally closure.Promiseto capture unhandled rejections; the wrapper might run cleanup against a partially-deinitialized closure scope on a hot-reload or bundle-eviction path.We can't reproduce on demand — it's intermittent, low-spec-Android only, and only fires when
negotiate()rejects (e.g. ICE candidate exhaustion, signal disconnect mid-negotiate).Related upstream history
EngineEvent.Closinglistener pattern).Workaround applied on our side
Sentry
beforeSendfilter that drops events matching the exact 4-clause signature (TypeError + exact message + livekit-client.umd.js frame + onunhandledrejection mechanism). This stops the noise in our Sentry but doesn't fix anything for users — the negotiation reconnect path is still silently broken whenever this fires.Ask
negotiate()finally-block defensively guard againstEngineEventbeing undefined (e.g. captureEngineEvent.Closing/EngineEvent.Restartinginto local consts at function entry, before the await)? That would make the finally block robust regardless of namespace resolution timing.Happy to provide more event data, runtime tags, or a sanitized stack export if useful.