margelo / margelo/react-native-vision-camera

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

Open
#4,069 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
9.6k
Forks
1.4k
Avg merge
1d 28m
Merged PRs (30d)
4

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/hooks/internal/useTorchModeUpdater.ts and android/.../HybridCameraController.kt around setTorchMode, then reproduce with a flash-less front camera and torchMode="off". Done means the native no-flash case no longer causes an unhandled rejection and torch updates still behave correctly on cameras with a flash unit.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin, react-native, typescript
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.