What's happening?
Setting the torchMode prop to any value — including 'off' — on a camera whose bound camera has no flash unit produces an unhandled promise rejection on every mount/update:
Error: java.lang.IllegalStateException: No flash unit
at androidx.camera.camera2.impl.TorchControl.setTorchAsync-Oup_wC0$camera_camera2(TorchControl.kt:128)
at androidx.camera.camera2.adapter.CameraControlAdapter.enableTorch(CameraControlAdapter.kt:115)
at androidx.camera.core.impl.AdapterCameraControl.enableTorch(AdapterCameraControl.java:73)
at com.margelo.nitro.camera.hybrids.HybridCameraController$setTorchMode$1.invokeSuspend(HybridCameraController.kt:205)
...
mechanism: onunhandledrejection
Two compounding causes in react-native-vision-camera@5.1.0:
useTorchModeUpdater never handles rejection (src/hooks/internal/useTorchModeUpdater.ts):
useEffect(() => {
if (controller == null) return
if (torchMode == null) return
controller.setTorchMode(torchMode) // ← returned Promise is neither awaited nor .catch()-ed
}, [controller, torchMode])
Any native failure becomes a global unhandled rejection the app cannot intercept (there is no onError route for it, and no imperative handle to try/catch).
setTorchMode calls enableTorch unconditionally (android/.../HybridCameraController.kt:200): CameraX's TorchControl fails the returned future with IllegalStateException("No flash unit") for any enableTorch(...) call on a camera without a flash unit — even enableTorch(false). There is no hasFlashUnit pre-check.
The failure mode is easy to hit accidentally: an app that renders torchMode={torchOn ? 'on' : 'off'} (a natural way to write a declarative prop) emits one Sentry-reported unhandled rejection per mount of any flash-less camera — the front camera included. In our production app (Sentry ID ALLY-HOME-NATIVE-22) this produced 4,438 events across 190 users in 5 days, almost all from front-camera screens where the torch was never touched.
It can also occur with a correctly-guarded prop: on some logical multi-camera devices (observed on a Galaxy Z Fold) device.hasTorch is true while the bound physical camera has no flash unit, so even torchMode='on' behind a hasTorch check rejects.
Reproduction
<Camera device={frontDevice} isActive torchMode="off" ... /> (or any device whose bound camera lacks a flash unit) on Android.
- Observe an unhandled promise rejection
java.lang.IllegalStateException: No flash unit on mount — no torch interaction needed.
Suggested fix
Either (ideally both):
- In
useTorchModeUpdater, handle the promise: controller.setTorchMode(torchMode).catch(...) — route to the camera onError handler or log, so library-internal failures can't become app-level unhandled rejections. (Same pattern applies to the other fire-and-forget updaters.)
- In the Android
setTorchMode, no-op (or reject with a typed, documented error) when the camera has no flash unit — camera.cameraInfo.hasFlashUnit() — at minimum for the OFF case, which is always semantically a no-op on a flash-less camera.
Workaround
Pass torchMode={hasTorch ? (torchOn ? 'on' : 'off') : undefined} — useTorchModeUpdater skips null/undefined, so the native call never happens. This doesn't cover the hasTorch-mismatch case on logical multi-cameras.
Environment
- react-native-vision-camera 5.1.0 (Nitro), react-native 0.86.0, Expo SDK 57, New Architecture
- Android (CameraX path); observed on Samsung SM-A156U (Galaxy A15), Galaxy Z Fold, and other devices
What's happening?
Setting the
torchModeprop to any value — including'off'— on a camera whose bound camera has no flash unit produces an unhandled promise rejection on every mount/update:Two compounding causes in
react-native-vision-camera@5.1.0:useTorchModeUpdaternever handles rejection (src/hooks/internal/useTorchModeUpdater.ts):Any native failure becomes a global unhandled rejection the app cannot intercept (there is no
onErrorroute for it, and no imperative handle to try/catch).setTorchModecallsenableTorchunconditionally (android/.../HybridCameraController.kt:200): CameraX'sTorchControlfails the returned future withIllegalStateException("No flash unit")for anyenableTorch(...)call on a camera without a flash unit — evenenableTorch(false). There is nohasFlashUnitpre-check.The failure mode is easy to hit accidentally: an app that renders
torchMode={torchOn ? 'on' : 'off'}(a natural way to write a declarative prop) emits one Sentry-reported unhandled rejection per mount of any flash-less camera — the front camera included. In our production app (Sentry ID ALLY-HOME-NATIVE-22) this produced 4,438 events across 190 users in 5 days, almost all from front-camera screens where the torch was never touched.It can also occur with a correctly-guarded prop: on some logical multi-camera devices (observed on a Galaxy Z Fold)
device.hasTorchistruewhile the bound physical camera has no flash unit, so eventorchMode='on'behind ahasTorchcheck rejects.Reproduction
<Camera device={frontDevice} isActive torchMode="off" ... />(or any device whose bound camera lacks a flash unit) on Android.java.lang.IllegalStateException: No flash uniton mount — no torch interaction needed.Suggested fix
Either (ideally both):
useTorchModeUpdater, handle the promise:controller.setTorchMode(torchMode).catch(...)— route to the cameraonErrorhandler or log, so library-internal failures can't become app-level unhandled rejections. (Same pattern applies to the other fire-and-forget updaters.)setTorchMode, no-op (or reject with a typed, documented error) when the camera has no flash unit —camera.cameraInfo.hasFlashUnit()— at minimum for theOFFcase, which is always semantically a no-op on a flash-less camera.Workaround
Pass
torchMode={hasTorch ? (torchOn ? 'on' : 'off') : undefined}—useTorchModeUpdaterskipsnull/undefined, so the native call never happens. This doesn't cover thehasTorch-mismatch case on logical multi-cameras.Environment