Summary
On the New Architecture (bridgeless), importing react-native-blob-util can intermittently crash at startup with:
Invariant Violation: new NativeEventEmitter() requires a non-null argument.
The package constructs a NativeEventEmitter at module top-level in two files, passing the TurboModule spec resolved via the nullable TurboModuleRegistry.get('ReactNativeBlobUtil'). During a bridgeless cold start, the module can be imported before that TurboModule has registered, so the spec is null and the eager new NativeEventEmitter(null) trips the iOS-only invariant.
Environment
- react-native-blob-util: 0.24.10 (also present on master)
- React Native: 0.81+ with New Architecture / bridgeless enabled (repro'd on RN 0.86 / Expo SDK 57)
- Platform: iOS
Root cause
Two module-scope constructions, both unguarded:
fetch.js:10 — const eventEmitter = new NativeEventEmitter(ReactNativeBlobUtil);
class/ReactNativeBlobUtilReadStream.js:10 — const emitter = new NativeEventEmitter(ReactNativeBlobUtil);
where ReactNativeBlobUtil is:
codegenSpecs/NativeBlobUtils.js:84 — export default (TurboModuleRegistry.get<Spec>('ReactNativeBlobUtil'): ?Spec);
TurboModuleRegistry.get() (unlike getEnforcing()) returns null when the module isn't registered yet. Because both constructions run at import time and fs.js/fetch.js (and therefore index.js) pull in ReactNativeBlobUtilReadStream, simply importing the package during the cold-start window is enough to crash. It's a race, so it's intermittent — more likely on slower devices / heavier startups.
This is the same class of bug the React Native team fixed in core Keyboard.js by deferring the emitter construction.
Reproduction
- New Arch (bridgeless) app on iOS.
- Import react-native-blob-util early in startup (e.g. transitively, before first render).
- Cold-launch repeatedly — a fraction of launches throw the invariant during module init.
Proposed fix
Construct each NativeEventEmitter lazily on first use behind a small getter, so it happens after the TurboModule has registered. Behaviour is unchanged for real usage — the emitters are only used to addListener inside fetch() and the read-stream constructor. Sketch:
// fetch.js
let eventEmitter = null;
function getEventEmitter() {
if (eventEmitter == null) {
eventEmitter = new NativeEventEmitter(ReactNativeBlobUtil);
eventEmitter.addListener('ReactNativeBlobUtilMessage', /* … */);
}
return eventEmitter;
}
// …then getEventEmitter().addListener(…) at each call site.
Same treatment for ReactNativeBlobUtilReadStream.js.
One behaviour note worth a maintainer decision: the global ReactNativeBlobUtilMessage diagnostic listener currently registers at import; deferring it to first fetch() means a process that only uses the fs/file:// APIs wouldn't receive that global diagnostic (per-operation handling is unaffected). If that matters, the message listener could be attached from a shared first-use hook covering both the HTTP and fs paths.
I'm running this as a local patch and happy to open a PR if the approach looks good.
Summary
On the New Architecture (bridgeless), importing react-native-blob-util can intermittently crash at startup with:
Invariant Violation:
new NativeEventEmitter()requires a non-null argument.The package constructs a NativeEventEmitter at module top-level in two files, passing the TurboModule spec resolved via the nullable
TurboModuleRegistry.get('ReactNativeBlobUtil'). During a bridgeless cold start, the module can be imported before that TurboModule has registered, so the spec is null and the eagernew NativeEventEmitter(null)trips the iOS-only invariant.Environment
Root cause
Two module-scope constructions, both unguarded:
fetch.js:10—const eventEmitter = new NativeEventEmitter(ReactNativeBlobUtil);class/ReactNativeBlobUtilReadStream.js:10—const emitter = new NativeEventEmitter(ReactNativeBlobUtil);where ReactNativeBlobUtil is:
codegenSpecs/NativeBlobUtils.js:84—export default (TurboModuleRegistry.get<Spec>('ReactNativeBlobUtil'): ?Spec);TurboModuleRegistry.get()(unlikegetEnforcing()) returns null when the module isn't registered yet. Because both constructions run at import time andfs.js/fetch.js(and thereforeindex.js) pull inReactNativeBlobUtilReadStream, simply importing the package during the cold-start window is enough to crash. It's a race, so it's intermittent — more likely on slower devices / heavier startups.This is the same class of bug the React Native team fixed in core Keyboard.js by deferring the emitter construction.
Reproduction
Proposed fix
Construct each NativeEventEmitter lazily on first use behind a small getter, so it happens after the TurboModule has registered. Behaviour is unchanged for real usage — the emitters are only used to addListener inside fetch() and the read-stream constructor. Sketch:
Same treatment for
ReactNativeBlobUtilReadStream.js.One behaviour note worth a maintainer decision: the global ReactNativeBlobUtilMessage diagnostic listener currently registers at import; deferring it to first fetch() means a process that only uses the fs/file:// APIs wouldn't receive that global diagnostic (per-operation handling is unaffected). If that matters, the message listener could be attached from a shared first-use hook covering both the HTTP and fs paths.
I'm running this as a local patch and happy to open a PR if the approach looks good.