stablyai/orca · error

Microphone permission denied

Error message

Microphone permission denied

What it means

Thrown by the start callback in use-mobile-dictation after requestMicrophonePermissionsAsync() returned a result with permission.granted === false. This is the user-level permission gate — the OS-level mic permission was denied (or restricted), so the hook refuses to proceed to initialize()/start.

Source

Thrown at mobile/src/hooks/use-mobile-dictation.ts:132

    const client = clientRef.current
    if (!client || !enabledRef.current || activeIdRef.current) {
      return
    }

    const generation = generationRef.current + 1
    generationRef.current = generation
    setError(null)
    setStatus('starting')
    const permission = await requestMicrophonePermissionsAsync()
    if (generationRef.current !== generation || !enabledRef.current) {
      if (generationRef.current === generation) {
        setStatus('idle')
      }
      return
    }
    if (!permission.granted) {
      setStatus('idle')
      throw new Error('Microphone permission denied')
    }

    const initialized = await initialize()
    if (generationRef.current !== generation || !enabledRef.current) {
      void tearDown()
      if (generationRef.current === generation) {
        setStatus('idle')
      }
      return
    }
    if (!initialized) {
      setStatus('idle')
      throw new Error('Failed to initialize microphone')
    }

    const dictationId = createMobileDictationId()
    activeIdRef.current = dictationId

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Surface a deep-link to the OS settings page so the user can re-enable the microphone permission (iOS: app settings, Android: package settings).
  2. Distinguish 'denied' from 'blocked'/restricted and tailor the copy — a restricted state needs an admin/MDM fix, not a settings toggle.
  3. Re-request only when the user explicitly taps a 'request again' affordance; do not loop requestMicrophonePermissionsAsync on mount.
  4. After the user returns from settings, re-run start() — the permission will now report granted.

Example fix

// before
const permission = await requestMicrophonePermissionsAsync()
if (!permission.granted) {
  setStatus('idle')
  throw new Error('Microphone permission denied')
}

// after — branch on the denial reason so the UI can deep-link settings
const permission = await requestMicrophonePermissionsAsync()
if (!permission.granted) {
  setStatus('idle')
  const reason = permission.canRequestAgain === false ? 'blocked' : 'denied'
  throw new Error(`Microphone permission ${reason}`)
}
Defensive patterns

Strategy: validation

Validate before calling

// Check current permission status without prompting first
const status = await getMicrophonePermissionStatusAsync()
if (status !== 'granted') {
  openAppSettings()
  return
}

Type guard

function isMicrophonePermissionError(err: unknown): boolean {
  return err instanceof Error && err.message === 'Microphone permission denied'
}

Try / catch

try {
  await start()
} catch (err) {
  if (isMicrophonePermissionError(err)) {
    showPermissionDeniedUI({ onOpenSettings: openAppSettings })
    return
  }
  throw err
}

Prevention

When it happens

Trigger: await requestMicrophonePermissionsAsync() resolves with {granted:false}. Reached only after the generation/enabled staleness check passes. Causes: user tapped 'Don't Allow' on the OS permission prompt, permission is restricted by MDM/screen-time policy, the prompt was previously denied and iOS now returns .denied without a prompt.

Common situations: First-time user denied the prompt; user previously denied and is retrying (iOS now needs Settings toggle); enterprise MDM blocks mic; Android permission was revoked in system settings while the app was backgrounded.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/8e41b6a451573e8d. Report an issue: GitHub.