skip to content

In expo-local-authentication, what does authenticateAsync resolve with, and how should a React Native app handle a cancelled or failed prompt?

level: middleimportance: should knowfreq 40%

answer

  1. resolves, does not reject, on failure
  2. success flag plus an error string
  3. user_cancel vs lockout vs not_enrolled
  4. disableDeviceFallback defaults to false
  5. Android biometricsSecurityLevel defaults to weak

basics

~20 s

authenticateAsync resolves with { success: true } or { success: false, error, warning? }; a cancel or failed scan is a resolved result, not a rejection. The app branches on error: retry after a cancel, fall back to password sign-in after lockout.

solid answer

~40 s

`LocalAuthentication.authenticateAsync(options)` shows Face ID, Touch ID or the Android biometric prompt and resolves with `{ success: true }` or `{ success: false, error, warning? }`. Cancellation and failure **resolve** with an error string such as `user_cancel`, `system_cancel`, `lockout`, `not_enrolled` or `user_fallback`; the promise rejects only when the module is unavailable, the options are invalid or an internal error occurs. So you check `hasHardwareAsync()` and `isEnrolledAsync()` first, then branch: a user cancel leaves the lock screen with a retry button, lockout or `user_fallback` routes to password sign-in. Options matter: `disableDeviceFallback` defaults to `false`, which lets the device passcode or PIN pass the prompt, and on Android `biometricsSecurityLevel` defaults to `'weak'`.

code

typescript · 29 lines
typescript
import * as LocalAuthentication from 'expo-local-authentication';

type UnlockOutcome = 'unlocked' | 'retry' | 'password';

export async function unlockPortfolio(): Promise<UnlockOutcome> {
  const [hasHardware, enrolled] = await Promise.all([
    LocalAuthentication.hasHardwareAsync(),
    LocalAuthentication.isEnrolledAsync(),
  ]);
  if (!hasHardware || !enrolled) return 'password';

  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: 'Unlock your portfolio',
    cancelLabel: 'Not now',
    fallbackLabel: 'Use password',
    disableDeviceFallback: true,
    biometricsSecurityLevel: 'strong',
  });

  if (result.success) return 'unlocked';
  switch (result.error) {
    case 'user_cancel':
    case 'system_cancel':
    case 'app_cancel':
      return 'retry';
    default:
      return 'password'; // lockout, user_fallback, not_enrolled, ...
  }
}

go deeper

for a junior

Know the shape: the call resolves with success true or success false plus an error code, and a cancel is not an exception.

for a middle

Explain the pre-checks, the error codes worth branching on, and what disableDeviceFallback and biometricsSecurityLevel change about who can pass.

for a senior

Show the product judgement: which failures retry, which fall back to password, and why a successful prompt alone does not protect the stored token.

for a principal

Weigh convenience against assurance: when device-passcode fallback is acceptable for unlocking a portfolio view but not for moving money.

## What the call does `expo-local-authentication` wraps Face ID and Touch ID on iOS and the Android biometric prompt. Its main function, `authenticateAsync(options)`, shows the system prompt and tells your JavaScript whether the user passed it. It is a **presence check**: it answers "is the device owner here right now?" It does not read or unlock any stored data by itself. Face ID does not work in Expo Go; you need a development build to test it on an iPhone. ## The result shape The promise resolves with a `LocalAuthenticationResult`: - `{ success: true }` when the user authenticated. - `{ success: false, error, warning? }` otherwise, where `error` is a string code. This is the part candidates get wrong: **a cancelled or failed prompt resolves; it does not reject.** A `try/catch` around the call catches only unavailability, invalid options such as an empty `promptMessage`, or internal errors, so code that treats "no exception" as "authenticated" is broken. | `error` value | Typical cause | Sensible reaction in a brokerage app | |---|---|---| | `user_cancel` | user dismissed the prompt or pressed the cancel button | stay on the lock screen, offer "Try again" | | `system_cancel`, `app_cancel` | the OS or the app interrupted the prompt | retry when the app is active again | | `user_fallback` | user tapped the iOS fallback button | show the password sign-in | | `lockout` | too many failed attempts | password sign-in; biometrics are blocked for now | | `not_enrolled`, `not_available`, `passcode_not_set` | nothing to authenticate with | password sign-in, maybe suggest enrolling | Not every code occurs on both platforms; `user_fallback`, for instance, comes from iOS. ## Check before you prompt A few helpers let you avoid showing a prompt that cannot succeed: - `hasHardwareAsync()` returns whether a face or fingerprint sensor exists. - `isEnrolledAsync()` returns whether any biometric data is enrolled. - `supportedAuthenticationTypesAsync()` returns the available kinds (fingerprint, facial recognition, iris on Android). - `getEnrolledLevelAsync()` returns a `SecurityLevel`: `NONE`, `SECRET` (PIN or pattern only), `BIOMETRIC_WEAK` or `BIOMETRIC_STRONG`. ## Options that change the security of the check | Option | Platform | Default | Effect | |---|---|---|---| | `promptMessage` | both | `'Authenticate'` | text shown in the prompt | | `cancelLabel` | both | `'Cancel'` | label of the cancel button (on Android it is used when device fallback is disabled) | | `disableDeviceFallback` | both | `false` | when `true`, the device passcode or PIN cannot satisfy the prompt | | `fallbackLabel` | iOS | system label | the fallback button's text; an empty string hides it | | `biometricsSecurityLevel` | Android | `'weak'` | `'strong'` allows only Class 3 biometrics such as fingerprint or 3D face | The default is convenient but permissive: with `disableDeviceFallback: false`, anyone who knows the phone's PIN passes the check. For confirming a trade you may want `disableDeviceFallback: true` and, on Android, `biometricsSecurityLevel: 'strong'`, with your own password screen as the fallback. ## A brokerage unlock flow 1. Call `hasHardwareAsync()` and `isEnrolledAsync()`; if either is false, go straight to password sign-in. 2. Call `authenticateAsync` with a clear `promptMessage` and the fallback policy you chose. 3. On `success: true`, continue. 4. On a cancel code, keep the lock screen and let the user retry. 5. On `lockout`, `user_fallback` or an unavailability code, show password sign-in. ## Common mistakes - Treating "no exception" as "authenticated" instead of reading `success`. - Prompting while the app is still in the background or mid-transition, then being surprised by a `system_cancel` or `app_cancel` result. - Firing a second call while one prompt is open; on Android the library answers the overlapping call with `app_cancel` rather than stacking prompts. - Logging the user out on `user_cancel`, which punishes someone who simply tapped the wrong button. - Testing Face ID in Expo Go and concluding the feature is broken. ## The limit to remember The resolved `success` is a value in JavaScript. It proves the prompt passed; it does not cryptographically protect the refresh token. Binding the token itself to biometrics is done with an access-control flag on the stored item, which is a separate design decision.

  • Why might a security review flag the default `disableDeviceFallback: false` for a trade confirmation?
    With the default, the prompt accepts the device passcode or PIN as well as biometrics. Someone who has watched the owner unlock the phone can then confirm a trade. Setting it to `true`, and on Android asking for `'strong'` biometrics, narrows the check to the enrolled biometric and sends everyone else to the app's own password screen.
  • What does `getEnrolledLevelAsync()` tell you that `isEnrolledAsync()` does not?
    It returns a `SecurityLevel`: `NONE`, `SECRET` for a PIN or pattern only, `BIOMETRIC_WEAK` or `BIOMETRIC_STRONG`. That lets the app distinguish a device protected only by a PIN from one with strong biometrics before deciding whether to offer biometric unlock at all.

saying these in an interview costs you the question

  • authenticateAsync rejects the promise when the user cancels.
  • No exception thrown means the user authenticated.
  • disableDeviceFallback defaults to true, so a PIN never passes.
  • A successful prompt decrypts the stored refresh token.
  • Face ID can be tested in Expo Go on an iPhone.