skip to content

Why does a React Native iOS app crash when it first opens the camera if Info.plist lacks NSCameraUsageDescription, and how do you fix it?

level: middleimportance: should knowfreq 46%

answer

  1. iOS demands a purpose string first
  2. missing key: the system terminates the app
  3. one NS*UsageDescription key per resource
  4. set in Info.plist or ios.infoPlist
  5. native change: new binary, not OTA

basics

~20 s

iOS requires a purpose string for each protected resource before it shows a prompt; without NSCameraUsageDescription it terminates the app on camera access. Add a specific description to Info.plist, or ios.infoPlist in Expo, and ship a new binary.

solid answer

~40 s

On iOS, the permission prompt shows text the app supplies in `Info.plist`, one **usage-description key** per resource: `NSCameraUsageDescription` for the camera, `NSPhotoLibraryUsageDescription` to read the photo library, `NSPhotoLibraryAddUsageDescription` to only save to it, `NSMicrophoneUsageDescription`, `NSFaceIDUsageDescription` and so on. If code touches a protected resource and the key is missing, iOS does not prompt — it terminates the app. React Native core has no iOS permission API, so the access comes from the native camera or photo library module, and the fix is native configuration: add the key to `ios/<App>/Info.plist` in a bare app, or to `ios.infoPlist` (or a library's config-plugin option) in an Expo app. Write a specific reason, because the store reviews it, and remember that `Info.plist` changes need a new binary; an OTA update cannot add them.

code

json · 11 lines
json
{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "The scanner uses the camera to photograph receipts and contracts you want to scan.",
        "NSPhotoLibraryUsageDescription": "Choose existing photos of documents to turn them into scans.",
        "NSPhotoLibraryAddUsageDescription": "Save finished scans to your photo library."
      }
    }
  }
}

go deeper

for a junior

Recall that every protected iOS resource needs an NS*UsageDescription string in Info.plist, and that a missing one terminates the app on first access.

for a middle

Explain where the key lives in bare and Expo projects, why only a new binary can add it, and how read and add-only photo keys differ.

for a senior

Build it into release hygiene: audit each native library's required keys, tailor purpose strings, and test first-run access on a clean install before submission.

for a principal

Own the privacy surface across apps: which resources the product may request at all, and who approves new purpose strings before they reach users and review.

## How iOS permission prompts work On iOS, access to the camera, microphone, photo library, location, contacts, Face ID and similar resources is gated by a **system prompt**. The prompt's body text is not written by the OS: it comes from a **purpose string** the app declares in its `Info.plist` under a resource-specific key. iOS treats that string as a precondition. If the app's code reaches a protected resource and the matching key is absent, the system does not fall back to a generic prompt; it **terminates the app**, which looks like a crash on first use. In a React Native app the access never comes from JavaScript directly. React Native core has no iOS permission API — `PermissionsAndroid` is Android-only — so the prompt is triggered by a native module: a camera library's request function, a photo-library module, a biometric module or a permissions library. All of them depend on the same `Info.plist` keys. ## The keys a document-scanning app needs | Resource | Key | Needed when | |---|---|---| | Camera | `NSCameraUsageDescription` | capturing pages with the camera | | Photo library, read | `NSPhotoLibraryUsageDescription` | reading the user's library through a library module | | Photo library, add only | `NSPhotoLibraryAddUsageDescription` | saving a scan into Photos without reading it | | Microphone | `NSMicrophoneUsageDescription` | recording video with audio | | Face ID | `NSFaceIDUsageDescription` | unlocking saved scans with Face ID | Face ID behaves slightly differently in one common module: `expo-local-authentication` checks for `NSFaceIDUsageDescription` itself and, when it is missing, authenticates with the device passcode instead, or resolves with the error `missing_usage_description` when device fallback is disabled, because calling Face ID would crash. ## Fixing it in each project type 1. **Bare React Native** — add the key and a string to `ios/<App>/Info.plist`, most easily in Xcode, then rebuild. 2. **Expo** — set it under `ios.infoPlist` in the app config, or use the option the library's config plugin exposes; prebuild then writes it into the generated `Info.plist`. 3. **Either way** — a new native build is required. The Expo docs state that `Info.plist` changes cannot be delivered over the air. ## Writing a purpose string that passes review The Expo permissions guide notes that libraries' config plugins insert boilerplate strings such as "Allow $(PRODUCT_NAME) to use Face ID", and that these usually need tailoring for the App Store to accept the app. A good string: - names the **feature**: "to photograph receipts and contracts you want to scan"; - matches what the app actually does with the data; - stays short, because it sits inside a system alert; - is localized along with the rest of the app. ## Android is the opposite shape | | iOS | Android | |---|---|---| | Declaration | purpose string in `Info.plist` | `<uses-permission>` in `AndroidManifest.xml` | | Missing declaration | app terminated on access | request refused without a prompt | | Explanation text | the purpose string, inside the system prompt | your own UI, or the `PermissionsAndroid` rationale | | After refusal | prompt never shown again | prompt usually shown again until a permanent refusal | ## Diagnosing the crash The failure is easy to misread as a bug in the camera library, because it happens exactly when the library first touches the camera. Two clues point at the purpose string instead: - the device log at the moment of termination names the missing usage-description key; - the crash reproduces only on first access, on every device, and disappears once the key is added — no code change needed. A related trap is a **dependency you did not expect**. A native library can reference a protected API — the photo library, location, the microphone — even if your app never calls that part. App Store processing can flag a build whose binary references such an API without the matching key, so audit what each native dependency links, not only what your code calls. ## Checklist before release - Every native library you added: which `Info.plist` keys does it need? - Is each purpose string specific to your feature, not the library's default? - Does a first-run test on a clean simulator hit every protected resource without a crash?

  • A React Native team adds a scanning library and ships the JavaScript through an OTA update; iOS users crash on the Scan button. Why?
    Two native changes were skipped: the library's native code and its `Info.plist` purpose string only arrive in a new binary. An OTA update replaces JavaScript and assets, not native code or `Info.plist`, so the button calls into a module or a protected resource the installed build cannot serve. Ship a new build, and gate the feature on the native version.
  • Why does saving a finished scan to Photos need a different key from importing one?
    iOS separates add-only access, `NSPhotoLibraryAddUsageDescription`, from read access, `NSPhotoLibraryUsageDescription`. An app that only saves should ask for the narrower one, which users grant more readily and which exposes none of their existing photos.

saying these in an interview costs you the question

  • PermissionsAndroid also shows the iOS camera prompt.
  • Without the purpose string iOS shows a generic prompt instead.
  • An OTA update can add a missing Info.plist key.
  • The default config-plugin string is always good enough for review.
  • Reading and saving photos use the same Info.plist key.