# App gracefully handles permission denials

- **Pattern:** `ab-001910` (`mobile-permissions-privacy.graceful-degradation.permission-denial-handling`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001910
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001910)

## Why it matters

A permission denial is a user's explicit choice to restrict access — an app that crashes in response to that choice punishes the user for exercising their privacy preference. This is both a UX failure and a reliability issue captured by CWE-391 (Unchecked Error Condition) and ISO 25010 reliability fault-tolerance requirements. On iOS, once a user denies a permission, the native dialog never appears again — the app must handle the BLOCKED state and guide the user to Settings. An app that shows a generic error message with no Settings deep-link leaves the user with no path forward. The pattern of crashing on denial also causes 1-star reviews specifically citing privacy concerns, compounding the trust damage.

## Severity rationale

Medium because permission-denial crashes create immediate user-facing failures and, for users exercising GDPR consent withdrawal, punish a legally protected user action with an application crash.

## Remediation

Handle all three permission states — GRANTED, DENIED, BLOCKED — for every permission-gated feature. For BLOCKED state, always provide a direct Settings deep-link.

```typescript
import { request, check, PERMISSIONS, RESULTS, openSettings } from 'react-native-permissions'

async function activateCameraFeature() {
  const status = await check(
    Platform.OS === 'ios' ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA
  )

  switch (status) {
    case RESULTS.GRANTED:
      startCamera()
      break
    case RESULTS.DENIED:
      const result = await request(
        Platform.OS === 'ios' ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA
      )
      if (result === RESULTS.GRANTED) startCamera()
      break
    case RESULTS.BLOCKED:
      Alert.alert(
        'Camera Access Required',
        'Camera access was denied. Open Settings to enable it.',
        [{ text: 'Open Settings', onPress: openSettings }, { text: 'Cancel' }]
      )
      break
    default:
      // Feature unavailable — disable UI element, do not crash
      setFeatureAvailable(false)
  }
}
```

## Detection

- **ID:** `permission-denial-handling`
- **Severity:** `medium`
- **What to look for:** List all permission-gated features. For each, check whether the code handles at least 3 permission states: GRANTED, DENIED, and BLOCKED. Look for try-catch blocks, null checks, or fallback UI for each denied/blocked path.
- **Pass criteria:** At least 100% of permission-gated features handle denial gracefully — showing a user-friendly message or disabling the feature without crashing. Each feature handles at least 2 denial states (DENIED and BLOCKED).
- **Fail criteria:** Any feature crashes or behaves unexpectedly when a permission is denied. OR displays a generic error with no guidance on how to grant the permission.
- **Skip (N/A) when:** App requests no permissions (no permission-gated features exist).
- **Detail on fail:** Quote the error handling code (or note its absence). Example: `"Camera permission denied causes app to crash in video-call component"` or `"Location permission denied shows generic 'error' message with no guidance"`
- **Cross-reference:** For error handling patterns and user-facing error recovery UX, the Error Resilience audit covers error boundaries and graceful degradation.
- **Remediation:** Handle permission denials gracefully:

  ```typescript
  async function startVideoCall() {
    const result = await request(PERMISSIONS.IOS.CAMERA)

    if (result === RESULTS.GRANTED) {
      // Start call
    } else if (result === RESULTS.DENIED) {
      Alert.alert('Camera Required', 'Enable camera access in Settings to make video calls')
    } else if (result === RESULTS.BLOCKED) {
      Alert.alert(
        'Camera Disabled',
        'You previously denied camera access. Go to Settings > [AppName] > Camera and toggle it on.',
        [
          { text: 'Cancel' },
          { text: 'Go to Settings', onPress: () => openSettings() }
        ]
      )
    }
  }
  ```

## External references

- cwe CWE-391
- iso-25010 reliability.fault-tolerance

Taxons: error-resilience, user-experience

HTML version: https://auditbuffet.com/patterns/ab-001910
