What happened?
onError never fires for a source that fails to load asynchronously (e.g. a 404 / unreachable URL), even though the docs describe it as the way to catch playback errors and show it wired up right next to player.play() with no caveat.
Root cause, traced in the core Nitro player:
- onError is dispatched only from VideoPlayer.ts's private
throwError(), called only when a promise from initialize()/preload()/replaceSourceAsync() rejects (VideoPlayer.ts:86-98, :254-256).
- On iOS, a bad source does not reject
initialize() - it resolves optimistically, and the failure surfaces later via AVPlayerItem's async status observer, which sets status = .error (HybridVideoPlayer+Events.swift:118-120). The status setter (HybridVideoPlayer.swift:175-184) emits onStatusChange('error') - a separate "shared" event path, while onError is explicitly "JS-only" (VideoPlayerEvents.native.ts:14-19).
Net effect: for the most common real-world error (bad URL/network failure at load), onError is silent; only onStatusChange('error') fires. Compounded by a second gotcha: with default initializeOnCreation: true, useVideoPlayer's setup callback (where listeners attach) is deferred until onLoadStart, which a failing source never reaches - so listeners never even get registered unless you set initializeOnCreation: false and call initialize() manually.
Found while building an E2E smoke-test harness and manually verifying a broken-source scenario on iOS Simulator - reproducible every run.
Expected: onError fires for a source that fails to load, matching the docs - or the docs/JSDoc should explicitly say onStatusChange('error') is the canonical signal instead.
Steps to reproduce
-
Create a player with a source that will fail to load (e.g. a 404 URL), and attach both onError and onStatusChange before calling play():
const player = useVideoPlayer(
{ uri: 'https://example.com/does-not-exist.mp4', initializeOnCreation: false },
(p) => {
p.addEventListener('onError', (e) => console.log('onError fired:', e));
p.addEventListener('onStatusChange', (s) => console.log('onStatusChange fired:', s));
p.initialize().then(() => p.play());
}
);
-
Render and watch the console/logs.
-
Observe the logs.
Reproduction repository
No response
react-native-video version
7.0.0-beta.11
react-native version
0.77.0
react-native-nitro-modules version
0.35.0
Platforms
iOS
OS version
iOS 26.5
Device
Simulator / Emulator
Architecture
New Architecture
Expo
No (bare / React Native CLI)
Last working version
No response
Media / source type
No response
What happened?
onErrornever fires for a source that fails to load asynchronously (e.g. a 404 / unreachable URL), even though the docs describe it as the way to catch playback errors and show it wired up right next toplayer.play()with no caveat.Root cause, traced in the core Nitro player:
throwError(), called only when a promise frominitialize()/preload()/replaceSourceAsync()rejects (VideoPlayer.ts:86-98, :254-256).initialize()- it resolves optimistically, and the failure surfaces later via AVPlayerItem's async status observer, which setsstatus = .error (HybridVideoPlayer+Events.swift:118-120). The status setter (HybridVideoPlayer.swift:175-184) emitsonStatusChange('error')- a separate "shared" event path, while onError is explicitly "JS-only" (VideoPlayerEvents.native.ts:14-19).Net effect: for the most common real-world error (bad URL/network failure at load),
onErroris silent; onlyonStatusChange('error')fires. Compounded by a second gotcha: with defaultinitializeOnCreation: true, useVideoPlayer's setup callback (where listeners attach) is deferred untilonLoadStart, which a failing source never reaches - so listeners never even get registered unless you setinitializeOnCreation: falseand callinitialize()manually.Found while building an E2E smoke-test harness and manually verifying a broken-source scenario on iOS Simulator - reproducible every run.
Expected:
onErrorfires for a source that fails to load, matching the docs - or the docs/JSDoc should explicitly sayonStatusChange('error')is the canonical signal instead.Steps to reproduce
Create a player with a source that will fail to load (e.g. a 404 URL), and attach both
onErrorandonStatusChangebefore callingplay():const player = useVideoPlayer(
{ uri: 'https://example.com/does-not-exist.mp4', initializeOnCreation: false },
(p) => {
p.addEventListener('onError', (e) => console.log('onError fired:', e));
p.addEventListener('onStatusChange', (s) => console.log('onStatusChange fired:', s));
p.initialize().then(() => p.play());
}
);
Render and watch the console/logs.
Observe the logs.
Reproduction repository
No response
react-native-video version
7.0.0-beta.11
react-native version
0.77.0
react-native-nitro-modules version
0.35.0
Platforms
iOS
OS version
iOS 26.5
Device
Simulator / Emulator
Architecture
New Architecture
Expo
No (bare / React Native CLI)
Last working version
No response
Media / source type
No response