Skip to content

Android: setTorchMode promise is fire-and-forget; 'off' on a flash-less camera (front camera) rejects with 'No flash unit' as an unhandled rejection on every mount #4069

Description

@nicksrandall

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:

  1. 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).

  1. 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

  1. <Camera device={frontDevice} isActive torchMode="off" ... /> (or any device whose bound camera lacks a flash unit) on Android.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions