---
url: 'https://www.corbado.com/blog/apple-passkey-error-codes'
title: 'Apple Passkey Error Codes: Complete Reference (2026)'
description: 'Complete reference of Apple''s passkey error codes across macOS 26.2 and iOS 26.2. Maps ASCAuthorizationError to ASAuthorizationError to DOMException.'
lang: 'en'
author: 'Vincent Delitz'
date: '2026-03-05T16:15:49.109Z'
lastModified: '2026-03-25T10:01:37.262Z'
keywords: 'apple passkey error codes, ASAuthorizationError, ASCAuthorizationError, iOS passkey errors, ASAuthorizationErrorFailed, LAError passkey, iCloud Keychain passkey error, NSUnderlyingErrorKey'
category: 'WebAuthn Know-How'
---

# Apple Passkey Error Codes: Complete Reference (2026)

## Key Facts

- Apple's passkey errors flow through a 3-layer pipeline: **ASCAuthorizationError**
  (private, codes 0-22) to **ASAuthorizationError** (public, codes 1000-1010) to
  DOMException in Safari.
- Apple has progressively extracted failures from generic code 1004: **1005** for no
  credentials (iOS 15), **1006** for duplicates (iOS 18), **1010** for device not
  configured (iOS 26).
- Safari translates native error codes to **DOMExceptions**, losing detail. Codes 1005,
  1009 and 1010 have no DOMException equivalent. Cancel (1001) and failure (1004) both
  become `NotAllowedError`.
- On iOS 26.2, **NSUnderlyingErrorKey** was nil for native passkey failures in testing.
  Use `localizedDescription` and `NSLocalizedFailureReasonErrorKey` for code 1004
  diagnostic detail.
- Using plain `default` in Swift silences compiler warnings when Apple adds new codes. Use
  **@unknown default** to be warned on SDK upgrades.

## 1. Introduction

Apple's [passkey error](https://www.corbado.com/blog/passkey-troubleshooting-solutions) system has multiple
layers and most developers only see the surface. The public `ASAuthorizationError` codes
(1000-1010) are intentionally broad - Apple compresses dozens of specific failure modes
into a handful of categories before they reach your app or Safari.

Under the hood, Apple's [passkey error](https://www.corbado.com/blog/passkey-troubleshooting-solutions) stack is
a 3-layer translation pipeline: specific codes from the AuthenticationServicesAgent
daemon, translated to public `ASAuthorizationError` codes in AuthenticationServices, then
translated again to DOMExceptions for Safari. At each layer, information is lost. This
article maps every layer completely.

This reference covers Apple's error codes as observed in macOS 26.2 (Tahoe),
[iOS 26](https://www.corbado.com/blog/ios-26-passkeys).2 and
[iOS 18](https://www.corbado.com/blog/ios-18-passkeys-automatic-passkey-upgrades).4 simulator runtimes. The error
code tables are identical between [iOS](https://www.corbado.com/blog/webauthn-errors) and macOS.

**What this article covers vs related articles:**

- For web browser errors (DOMExceptions like `NotAllowedError` and `AbortError`): see
  [WebAuthn Errors](https://www.corbado.com/blog/webauthn-errors) in Production
- For [Android](https://www.corbado.com/blog/how-to-enable-passkeys-android) error codes: see GPS
  [Passkey Error](https://www.corbado.com/blog/passkey-troubleshooting-solutions) Codes
- For native deployment strategy and abort rate analysis: see
  [Native App Passkey Errors](https://www.corbado.com/blog/native-app-passkey-errors)
- This article: the Apple error codes, 3-layer translation pipeline and Safari vs native
  visibility

## 2. Architecture: how Apple generates and routes Errors

Every passkey error on [iOS](https://www.corbado.com/blog/webauthn-errors) flows through a 3-layer pipeline
before reaching your app or Safari. Understanding these layers explains why logging only
`ASAuthorizationErrorFailed` can lead to missing the actual failure reason.

```mermaid
flowchart TD
  subgraph layer1 ["Layer 1: Private XPC Daemon"]
    ASCAgent["AuthenticationServicesAgent <br>(ASCAuthorizationErrorDomain) <br>Codes 0-22"]
  end

  subgraph layer2 ["Layer 2: Public Framework"]
    ASAuth["ASAuthorizationController<br>(ASAuthorizationErrorDomain)<br>Public codes 1000-1010"]
  end

  subgraph layer3_safari ["Layer 3a: Safari/WebKit"]
    Safari["WebAuthenticatorCoordinatorProxy<br>→ DOMException<br>(NotAllowedError, InvalidStateError...)"]
  end

  subgraph layer3_native ["Layer 3b: Native App"]
    NativeApp["Your app receives<br>ASAuthorizationError<br>+ full public code set (1000-1010)"]
  end

  subgraph biometric ["Biometric Layer"]
    LA["LAContext<br>(LAErrorDomain)<br>Codes -1 to -14"]
  end

  ASCAgent --> ASAuth
  LA --> ASCAgent
  ASAuth --> Safari
  ASAuth --> NativeApp
```

| Layer | Component                                    | Error domain                                   | What happens                                                                                                                                                                                                                                   |
| ----- | -------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1     | AuthenticationServicesAgent (XPC daemon)     | `ASCAuthorizationErrorDomain` (codes 0-22)     | Generates specific errors with user-visible messages like "Cannot create a passkey. iCloud Keychain is off."                                                                                                                                   |
| 2     | ASAuthorizationController (public framework) | `ASAuthorizationErrorDomain` (codes 1000-1010) | Translates `ASCAuthorizationError` to public codes. Many codes (4-11, 13, 20-21) have **no direct mapping** and are wrapped into `Failed` (1004) with the original preserved in `NSUnderlyingErrorKey`                                         |
| 3a    | Safari/WebKit                                | DOMException                                   | Translates `ASAuthorizationError` to DOMExceptions. Only three codes get specific handling: 1001 → `NotAllowedError`, 1006 → `InvalidStateError`, 1004 + security error → `SecurityError`. Everything else becomes generic `NotAllowedError`   |
| 3b    | Native apps                                  | `ASAuthorizationErrorDomain` (codes 1000-1010) | Receives the full set of public `ASAuthorizationError` codes. On iOS 18+, most common failures have dedicated codes (1005, 1006, 1010) that need no unwrapping. For code 1004, check `NSUnderlyingErrorKey` when present for additional detail |

> **The critical insight**: on [iOS
>     18](https://www.corbado.com/blog/ios-18-passkeys-automatic-passkey-upgrades)+ and [iOS
>     26](https://www.corbado.com/blog/ios-26-passkeys), most common passkey failures have dedicated public codes
>     (1005, 1006, 1010) that need no unwrapping. When you do receive
>     `ASAuthorizationErrorFailed` (1004), check `NSUnderlyingErrorKey` for the underlying
>     `ASCAuthorizationErrorDomain` code when present - but note that some 1004 errors (e.g.
>     domain association failures) report details in `localizedDescription` instead, with
>     `NSUnderlyingErrorKey` nil. Websites running in Safari lose all this detail because
>     WebKit translates errors to DOMExceptions before exposing them to JavaScript.

## 3. ASAuthorizationError (public API)

The public error domain that native apps and Safari see. 11 codes, stable across
[iOS](https://www.corbado.com/blog/webauthn-errors) versions with new additions.

| Code | Name                                    | Since    | What it means                                                                                                                                                                                                                                                                                            |
| ---- | --------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1000 | `Unknown`                               | iOS 13   | Unknown error. Should not appear in production                                                                                                                                                                                                                                                           |
| 1001 | `Canceled`                              | iOS 13   | **User dismissed the passkey sheet.** The most common error. On iOS, this is a clean signal unlike Android where user cancel is harder to distinguish                                                                                                                                                    |
| 1002 | `InvalidResponse`                       | iOS 13   | The authorization response was invalid. Rare - indicates framework-level corruption                                                                                                                                                                                                                      |
| 1003 | `NotHandled`                            | iOS 13   | The authorization request was not handled by any provider. Check your entitlements and configuration                                                                                                                                                                                                     |
| 1004 | `Failed`                                | iOS 13   | **Generic failure.** Check `NSUnderlyingErrorKey` for additional detail when present, but note that some 1004 errors (e.g. domain association failures) report details in `localizedDescription` instead. On iOS 18+, many scenarios that previously produced 1004 now have dedicated codes (1006, 1010) |
| 1005 | `NotInteractive`                        | iOS 15   | UI is required but the request was made with `preferImmediatelyAvailableCredentials`. This is the "no credential available" signal - the iOS equivalent of a clean "not found"                                                                                                                           |
| 1006 | `MatchedExcludedCredential`             | iOS 18   | A credential in the `excludeCredentials` list already exists on this device. Clean signal for duplicate detection                                                                                                                                                                                        |
| 1007 | `CredentialImport`                      | iOS 18.2 | Credential import operation failed                                                                                                                                                                                                                                                                       |
| 1008 | `CredentialExport`                      | iOS 18.2 | Credential export operation failed                                                                                                                                                                                                                                                                       |
| 1009 | `PreferSignInWithApple`                 | iOS 26   | User prefers to use Sign in with Apple instead of a passkey                                                                                                                                                                                                                                              |
| 1010 | `deviceNotConfiguredForPasskeyCreation` | iOS 26   | **New in iOS 26.** Device lacks passcode or iCloud Keychain configuration required for passkey creation. Previously this was hidden inside code 1004                                                                                                                                                     |

### 3.1 iOS Version Evolution: how Apple is emptying the 1004 Bucket

Apple has been systematically extracting specific failure modes out of the generic
`Failed` (1004) bucket across iOS versions. This is the most important practical trend for
passkey error handling:

| iOS version | What was extracted from 1004                             | New dedicated code | Impact                                                            |
| ----------- | -------------------------------------------------------- | ------------------ | ----------------------------------------------------------------- |
| iOS 15      | No credentials (`preferImmediatelyAvailableCredentials`) | 1005               | No longer need to string-match "no credentials" on 1004           |
| iOS 18      | Duplicate credential (`excludeCredentials` match)        | 1006               | No longer need `WKErrorDomain` code 8 fallback or string matching |
| iOS 26      | Device not configured (no passcode, iCloud Keychain off) | 1010               | No longer need to string-match "iCloud Keychain is off" on 1004   |

On [iOS 26](https://www.corbado.com/blog/ios-26-passkeys), the remaining cases that still need 1004 +
`NSUnderlyingErrorKey` unwrapping are increasingly narrow: primarily
[security key](https://www.corbado.com/glossary/security-key) operations (ASC codes 4, 8, 9, 10, 13, 20, 21)
which require physical hardware. Apple is gradually solving the information-loss problem
at the API level rather than requiring developers to dig into private error domains.

### 3.2 NSError userInfo Keys

When you receive an `ASAuthorizationError`, several userInfo keys carry diagnostic
information:

| Key                                         | Status             | What it contains                                                                                                                                                                                                                         |
| ------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NSUnderlyingErrorKey`                      | Public API         | The underlying error when present. Check on code 1004 but do not depend on it being populated - our testing found it nil for domain association failures. Most reliably present for WebKit-originated errors and security key operations |
| `NSMultipleUnderlyingErrorsKey`             | Public API         | Array of underlying errors - some ASAuthorization failures use this instead of the single-error key                                                                                                                                      |
| `NSLocalizedFailureReasonErrorKey`          | Public API         | Localized failure reason string. Often contains the actual diagnostic detail for code 1004 (e.g. "Application with identifier X is not associated with domain Y"). More reliably populated than `NSUnderlyingErrorKey` in our testing    |
| `ASAuthorizationErrorUserVisibleMessageKey` | Framework-specific | Human-readable message from the framework. Log when present but do not depend on it for user-facing messages - it may be absent or change across OS versions                                                                             |
| `ASExtensionLocalizedFailureReasonErrorKey` | Framework-specific | Localized failure reason from a credential provider extension (e.g. 1Password, Dashlane)                                                                                                                                                 |

Inspecting `NSUnderlyingErrorKey` is a standard iOS error handling practice, not something
specific to passkeys. Many iOS SDKs use this two-level unwrap pattern where the public
error code gives you the category and the underlying error gives you the root cause:

- [Firebase iOS Auth docs](https://firebase.google.com/docs/auth/ios/errors) explicitly
  state: _"When investigating or logging errors, review the `userInfo` dictionary.
  `NSUnderlyingErrorKey` contains the underlying error that caused the error in
  question."_
- [Atomic iOS SDK](https://development.documentation.atomic.io/sdks/ios) demonstrates
  casting to `NSError`, reading `userInfo[NSUnderlyingErrorKey]` and handling
  `@unknown default` across every error callback
- [RevenueCat's error utilities](https://sdk.revenuecat.com/ios/Classes/RCPurchasesErrorUtils.html)
  wrap backend and StoreKit errors into public error codes with the original preserved in
  `NSUnderlyingErrorKey`
- The same
  [domain/code matching pattern](https://gist.github.com/justinmeiners/6acff83ae434c4b3d855a0fbc9b7214e)
  applies to Foundation errors like `FileManager` where the surface error hides the actual
  POSIX cause

In theory, this same pattern should help dig deeper into passkey failures - inspecting the
underlying `ASCAuthorizationError` behind a generic `ASAuthorizationErrorFailed` (1004)
could reveal whether the cause is "[iCloud Keychain](https://www.corbado.com/glossary/icloud-keychain) is off" or
"[security key](https://www.corbado.com/glossary/security-key) PIN is locked." In practice however, our POC
testing on [iOS 26](https://www.corbado.com/blog/ios-26-passkeys).2 found that `NSUnderlyingErrorKey` was nil for
every native passkey scenario we triggered (domain association failures, missing
credentials). The diagnostic detail was in `localizedDescription` and
`NSLocalizedFailureReasonErrorKey` instead. This suggests that while
`NSUnderlyingErrorKey` is well-established in other Apple frameworks, passkey errors on
modern iOS rely more on dedicated public codes and localized description strings than on
the underlying error chain.

## 4. ASCAuthorizationError (private framework)

The private error domain generated by Apple's AuthenticationServicesAgent XPC daemon.
These codes are expected to surface via `NSUnderlyingErrorKey` at runtime based on
disassembly analysis, though runtime confirmation is limited to WebKit-originated and
device-configuration error paths. The following table covers codes present in macOS 26.2.

Note that ASC code 5 (no credentials) and code 7 (duplicate credential) - the two most
common "interesting" codes - are now surfaced via dedicated public codes on modern iOS:
code 5's scenario is handled by 1005 (`NotInteractive`) when using
`preferImmediatelyAvailableCredentials`, and code 7's scenario is handled by 1006
(`MatchedExcludedCredential`) on
[iOS 18](https://www.corbado.com/blog/ios-18-passkeys-automatic-passkey-upgrades)+. The primary value of this
ASCAuthorizationError table is for [security key](https://www.corbado.com/glossary/security-key) debugging and
understanding older iOS behavior.

| Code | User-visible message                                                       | Category                 | What it means                                                                                                                                               |
| ---- | -------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | (no description)                                                           | Unknown                  | Unknown error                                                                                                                                               |
| 1    | (no description)                                                           | General failure          | Generic failure                                                                                                                                             |
| 2    | (no description)                                                           | User canceled            | User canceled at the daemon layer                                                                                                                           |
| 4    | "Too many NFC devices found. Please present only one NFC device."          | NFC / security key       | Multiple NFC security keys detected simultaneously                                                                                                          |
| 5    | "Found no credentials on this device."                                     | No credentials           | **No passkey exists for this RP on this device.** Important diagnostic signal that surfaces as generic 1004 to apps that don't check `NSUnderlyingErrorKey` |
| 6    | "The operation cannot be completed."                                       | General failure          | Generic operation failure                                                                                                                                   |
| 7    | (biometry-dependent, see below)                                            | Duplicate credential     | A passkey already exists for this account                                                                                                                   |
| 8    | "Unrecognized PIN code. Please try again."                                 | Security key PIN         | Incorrect PIN entered for a hardware security key                                                                                                           |
| 9    | "Your security key is temporarily locked. Please remove it and try again." | Security key locked      | Too many recent PIN failures, temporary lockout                                                                                                             |
| 10   | "Too many invalid PIN attempts. Your security key must be reset."          | Security key PIN lockout | Permanent PIN lockout requiring security key reset                                                                                                          |
| 11   | (returns nil)                                                              | Unused/reserved          | No description assigned                                                                                                                                     |
| 12   | (returns nil)                                                              | Cancel variant           | Mapped to `ASAuthorizationErrorCanceled` (1001), no message needed                                                                                          |
| 13   | "Your security key's storage is full."                                     | Security key full        | The FIDO2 security key has no remaining credential slots                                                                                                    |
| 14   | (no description)                                                           | Invalid response         | Invalid response from the authenticator                                                                                                                     |
| 17   | (no description)                                                           | Device not configured    | Device not configured for passkey creation (maps to 1010 on iOS 26+)                                                                                        |
| 20   | "Pin entered was too short. Please choose a longer one."                   | Security key PIN         | PIN length validation failure (too short)                                                                                                                   |
| 21   | "Pin entered was too long. Please choose a shorter one."                   | Security key PIN         | PIN length validation failure (too long)                                                                                                                    |
| 22   | (no description)                                                           | Device not configured    | Second device-not-configured variant (also maps to 1010)                                                                                                    |

### 4.1 Code 7 - biometry-dependent Messages

Code 7 has three distinct code paths based on runtime device state:

| Condition                                | Message                                                                |
| ---------------------------------------- | ---------------------------------------------------------------------- |
| `shouldUseAlternateCredentialStore=true` | "You already have a passkey for this account in your iCloud Keychain." |
| `biometryType == 2` (Face ID)            | "You have already set up Face ID for this website."                    |
| `biometryType != 2` (Touch ID / other)   | "You have already set up Touch ID for this website."                   |

## 5. Error Code Translation Mapping

The 3-layer translation is the core of Apple's error pipeline. Here is the complete
mapping from ASCAuthorizationError to public
[ASAuthorizationError](https://www.corbado.com/blog/native-app-passkey-errors) as observed in macOS 26.2 and iOS
26.2.

| ASC Code        | Public Code | Public Name                     | Information lost?                                                                                                                                                                                                   |
| --------------- | ----------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0               | 1000        | `Unknown`                       | No                                                                                                                                                                                                                  |
| 1               | 1004        | `Failed`                        | Yes - generic wrapper                                                                                                                                                                                               |
| 2               | 1001        | `Canceled`                      | No                                                                                                                                                                                                                  |
| 12              | 1001        | `Canceled`                      | No                                                                                                                                                                                                                  |
| 14              | 1002        | `InvalidResponse`               | No                                                                                                                                                                                                                  |
| 17              | 1010        | `DeviceNotConfiguredForPasskey` | No (iOS 26+)                                                                                                                                                                                                        |
| 22              | 1010        | `DeviceNotConfiguredForPasskey` | No (iOS 26+)                                                                                                                                                                                                        |
| 4-11, 13, 20-21 | nil         | → wrapped in `Failed` (1004)    | These codes return nil from `_convertCoreErrorToPublicError:` and are inferred to be wrapped in `Failed` (1004) with `NSUnderlyingErrorKey`. Confirmed for the WebKit path; inferred for the native ASC daemon path |

## 6. What Websites see vs what RP native Apps see

Safari and native apps receive the same `ASAuthorizationError` from Layer 2, but Safari
then translates it to a DOMException (Layer 3a) while native apps retain the full public
code set. Codes 1005, 1006, 1009 and 1010 are native-app-only signals that Safari
collapses into generic DOMExceptions.

| ASAuthorizationError                                       | Code | DOMException         | Notes                                                                                           |
| ---------------------------------------------------------- | ---- | -------------------- | ----------------------------------------------------------------------------------------------- |
| `Canceled`                                                 | 1001 | `NotAllowedError`    | User dismissed the sheet. **Cannot be distinguished from other NotAllowedErrors in JavaScript** |
| `MatchedExcludedCredential`                                | 1006 | `InvalidStateError`  | Clean signal for excludeCredentials                                                             |
| `Failed` + `ASCAuthorizationErrorSecurityError` underlying | 1004 | `SecurityError`      | RP ID / origin mismatch                                                                         |
| `Failed` + other underlying                                | 1004 | `NotAllowedError`    | Generic fallback                                                                                |
| `Unknown`                                                  | 1000 | `ASSERT_NOT_REACHED` | Should never happen                                                                             |
| `InvalidResponse`                                          | 1002 | `ASSERT_NOT_REACHED` | Should never happen                                                                             |
| `NotHandled`                                               | 1003 | `ASSERT_NOT_REACHED` | Should never happen                                                                             |
| `NotInteractive`                                           | 1005 | `ASSERT_NOT_REACHED` | Should never happen                                                                             |
| `DeviceNotConfiguredForPasskey`                            | 1010 | (not handled)        | Falls through - new code not yet in WebKit translation                                          |

## 7. Common Pitfalls in Passkey Error Handling

Based on analysis of production [passkey SDKs](https://www.corbado.com/blog/best-passkey-sdks-libraries), these
are the most common mistakes in Apple passkey error handling:

- **String matching on `localizedDescription` for error classification.** Some SDKs use
  patterns like
  `error.localizedDescription.contains("No credentials available for login.")` or
  `error.localizedDescription.contains("is not associated with domain")`. This only works
  on English-language devices. Apple localizes these messages into 30+ languages. Use
  error codes for classification, not strings.
- **Mapping unknown codes to "cancelled."** Some SDKs have `default: → .cancelled` in
  their switch statement, which means codes 1005, 1009, 1010 all silently become "user
  cancelled." Unknown codes should map to "unknown" not "cancelled" - a code you don't
  recognize is not the same as a user dismissal.
- **Not handling codes added in newer iOS versions.** If your switch statement doesn't
  have cases for 1005, 1006, 1009, 1010, those codes fall into your default case. Add
  explicit cases for all known codes and use a safe default for future codes.
- **Using plain `default` instead of `@unknown default`.** Plain `default` silences the
  compiler when Apple adds new codes (which they've done in every major release recently:
  [iOS 15](https://www.corbado.com/blog/passkeys-ios-15) added 1005,
  [iOS 18](https://www.corbado.com/blog/ios-18-passkeys-automatic-passkey-upgrades) added 1006-1008, iOS 26 added
  1009-1010). Using `@unknown default` gives you a compiler warning on SDK upgrade,
  prompting you to handle new cases explicitly. The runtime behavior is identical.

A simple example of handling codes structurally, with `#available` checks for newer codes
and string matching only as a last resort for the ambiguous 1001 case:

```swift
func authorizationController(controller: ASAuthorizationController,
                             didCompleteWithError error: Error) {
    guard let authorizationError = error as? ASAuthorizationError else {
        handleUnknownError(error)
        return
    }

    // iOS 18+: dedicated code for duplicate credential
    if #available(iOS 18.0, macOS 15.0, *) {
        if authorizationError.code == .matchedExcludedCredential {
            handleDuplicateCredential()
            return
        }
    }

    // iOS 26+: dedicated codes for device config and Sign in with Apple preference
    if #available(iOS 26.0, macOS 26.0, *) {
        if authorizationError.code == .deviceNotConfiguredForPasskeyCreation {
            handleDeviceNotConfigured()
            return
        }
        if authorizationError.code == .preferSignInWithApple {
            handlePreferSignInWithApple()
            return
        }
    }

    switch authorizationError.code {
    case .canceled:  // 1001
        // On older iOS without preferImmediately, "no credentials"
        // can arrive as 1001 with a localized message.
        // String matching is a pragmatic last resort for this ambiguous case.
        if error.localizedDescription.contains("No credentials available") {
            handleNoCredentials()
        } else {
            handleUserCancelled()
        }

    case .notInteractive:  // 1005 (iOS 15+)
        // Clean signal: no credentials on this device for this RP.
        // No UI was shown. Offer passkey creation or fall back to password.
        handleNoCredentials()

    case .failed:  // 1004
        // Generic bucket. The localizedDescription contains the actual reason.
        // e.g. "Application with identifier X is not associated with domain Y"
        if error.localizedDescription.contains("not associated with domain") {
            handleDomainNotAssociated()
        } else {
            // Check NSUnderlyingErrorKey for security key details
            let nsError = error as NSError
            if let underlying = nsError.userInfo[NSUnderlyingErrorKey] as? NSError,
               underlying.domain == "ASCAuthorizationErrorDomain" {
                handleASCError(code: underlying.code)
            } else {
                handleGenericFailure(error)
            }
        }

    case .invalidResponse,
         .notHandled,
         .unknown:
        handleUnknownError(error)

    @unknown default:
        // IMPORTANT: do NOT map unknown codes to "cancelled".
        // @unknown default triggers a compiler warning when Apple adds new codes.
        handleUnknownError(error)
    }
}
```

## 8. LAError (biometric / LocalAuthentication)

Biometric failures fire before passkey operations complete. These errors come from
`LAContext` and surface as part of the passkey flow when
[user verification](https://www.corbado.com/blog/webauthn-user-verification) is required.

| Code  | Name                    | Since      | What it means                                                                             |
| ----- | ----------------------- | ---------- | ----------------------------------------------------------------------------------------- |
| -1    | `AuthenticationFailed`  | iOS 8      | User failed biometric or passcode verification                                            |
| -2    | `UserCancel`            | iOS 8      | User tapped Cancel on the biometric prompt                                                |
| -3    | `UserFallback`          | iOS 8      | User tapped "Enter Password" to use passcode instead of biometric                         |
| -4    | `SystemCancel`          | iOS 8      | System canceled (app went to background, incoming call)                                   |
| -5    | `PasscodeNotSet`        | iOS 8      | **No passcode configured.** Passkeys require this. Related to ASCAuthorizationError 17/22 |
| -6    | `BiometryNotAvailable`  | iOS 11     | Biometric hardware not available on this device                                           |
| -7    | `BiometryNotEnrolled`   | iOS 11     | No Face ID or Touch ID enrolled. Passkeys still work with passcode fallback               |
| -8    | `BiometryLockout`       | iOS 11     | Too many failed biometric attempts. Device requires passcode to unlock biometry           |
| -9    | `AppCancel`             | iOS 9      | Your app called `invalidate()` during authentication                                      |
| -10   | `InvalidContext`        | iOS 9      | The `LAContext` was previously invalidated                                                |
| -11   | `BiometryNotPaired`     | macOS 11.2 | Removable biometric accessory not paired (macOS only)                                     |
| -12   | `BiometryDisconnected`  | macOS 11.2 | Removable biometric accessory disconnected (macOS only)                                   |
| -14   | `CompanionNotAvailable` | iOS 18     | No paired companion device (e.g. Apple Watch) nearby for authentication                   |
| -1004 | `NotInteractive`        | iOS 8      | UI required but `interactionNotAllowed` was set to `true`                                 |

In practice, the most important LA errors for passkey reliability are `-5` (no passcode -
passkeys cannot work), `-8` (biometric lockout - temporary, passcode fallback available)
and `-4` (system cancel - transient, retry appropriate).

## 9. Additional error strings

Beyond the structured error codes, the Apple frameworks contain dozens of diagnostic
strings that appear in logs, crash reports and error `userInfo` dictionaries.

### 9.1 iCloud Keychain and Configuration

| String                                                                                           | Context                                                 |
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| "Cannot create a passkey. iCloud Keychain is off."                                               | Pre-flight check, surfaces as code 1004                 |
| "To save a passkey, you need to enable iCloud Keychain."                                         | User-facing prompt text                                 |
| "Passkeys are unavailable because iCloud Keychain has been disabled by a configuration profile." | MDM-managed devices                                     |
| "Passkeys require a passcode and work best with Face ID."                                        | Device setup guidance (iPhone)                          |
| "Passkeys require a passcode and work best with Touch ID."                                       | Device setup guidance (iPad, Mac)                       |
| "Passkeys require a passcode and work best with Optic ID."                                       | Device setup guidance (Apple Vision Pro, new in iOS 26) |
| "Passkeys require a passcode."                                                                   | Device without biometric hardware                       |

### 9.2 Access Control and Entitlements

| String                                                   | Context                                             |
| -------------------------------------------------------- | --------------------------------------------------- |
| "TCC access denied for browser passkey request."         | Transparency, Consent and Control permission denied |
| "Rejecting unentitled process from requesting passkeys." | Missing app entitlement                             |
| "Client is missing web browser entitlement."             | Browser-specific entitlement check                  |
| "Dropping passkey requests from quirked relying party."  | Apple maintains an RP quirks list                   |

## 10. Practical implications for error handling

### 10.1 What to log in Production

Always log beyond the `ASAuthorizationError` code. Three practical points: (1) for many
1004 errors, `localizedDescription` contains the actual diagnostic (e.g. "Application with
identifier X is not associated with domain Y") - not `NSUnderlyingErrorKey`, which may be
nil; (2) AuthenticationServices failures sometimes contain `NSMultipleUnderlyingErrorsKey`
instead of a single `NSUnderlyingErrorKey`; and (3) `NSError` objects in `userInfo` are
not JSON-serializable - convert to strings or dictionaries before sending to analytics.

```swift
func logPasskeyError(_ error: Error, operation: String) {
    let nsError = error as NSError

    var logEntry: [String: Any] = [
        "operation": operation,
        "domain": nsError.domain,
        "code": nsError.code,
        "description": String(describing: error)
    ]

    if let failureReason = nsError.localizedFailureReason {
        logEntry["failure_reason"] = failureReason
    }

    if let underlying = nsError.userInfo[NSUnderlyingErrorKey] as? NSError {
        logEntry["underlying"] = [
            "domain": underlying.domain,
            "code": underlying.code,
            "description": underlying.localizedDescription
        ]
    }

    if let multiple = nsError.userInfo[NSMultipleUnderlyingErrorsKey] as? [NSError] {
        logEntry["underlying_multiple"] = multiple.map {
            ["domain": $0.domain, "code": $0.code, "description": $0.localizedDescription]
        }
    }

    if let userMessage = nsError.userInfo["ASAuthorizationErrorUserVisibleMessageKey"] as? String {
        logEntry["user_visible_message"] = userMessage
    }

    analytics.log("passkey_error", properties: logEntry)
}
```

The robust fields (`domain`, `code`, `localizedDescription`, `localizedFailureReason`,
`NSUnderlyingErrorKey`) are public API and stable across iOS versions. The
`ASAuthorizationErrorUserVisibleMessageKey` is a framework-specific `userInfo` key - log
it when present but do not depend on it for user-facing messages. Instead, map known
`ASAuthorizationError.Code` values to your own strings for display and use the raw
underlying error details for debugging.

| Robust (public, stable)                                            | Best effort (may change)                                                              |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `domain`, `code`, `localizedDescription`, `localizedFailureReason` | Framework-specific `userInfo` keys like `"ASAuthorizationErrorUserVisibleMessageKey"` |
| `userInfo[NSUnderlyingErrorKey]` when present                      | Multiple underlying errors via `NSMultipleUnderlyingErrorsKey`                        |
| Mapping `ASAuthorizationError.Code` from the integer code          | Distinguishing some passkey outcomes that Apple intentionally collapses               |

### 10.2 Error Severity Classification

| Severity          | Codes                                                                                                   | Action                                                                                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Expected**      | 1001 (user cancel), 1005 (no credentials with `preferImmediatelyAvailable`), 1006 (excluded credential) | Normal flow outcomes. No action needed                                                                                                                                  |
| **Environmental** | 1010 (device not configured), ASC code 5 (no credentials), LAError -5 (no passcode)                     | Not fixable by your code. Suppress passkey prompts for these devices via [passkey intelligence](https://docs.corbado.com/corbado-connect/features/passkey-intelligence) |
| **Transient**     | LAError -4 (system cancel), LAError -8 (biometric lockout)                                              | Retry once or offer passcode fallback                                                                                                                                   |
| **Configuration** | 1003 (not handled), "iCloud Keychain is off", "TCC access denied"                                       | Fix your entitlements, AASA configuration or guide user to enable iCloud Keychain                                                                                       |
| **Platform**      | 1000 (unknown), 1002 (invalid response)                                                                 | Log and monitor. Likely Apple framework bugs                                                                                                                            |

For the broader native reliability picture including
[Android](https://www.corbado.com/blog/how-to-enable-passkeys-android) vs iOS abort rate analysis, see
[Native App Passkey Errors](https://www.corbado.com/blog/native-app-passkey-errors).

## 11. Conclusion

The key takeaway from Apple's passkey error stack is that **Apple is progressively solving
the information-loss problem at the API level.** Each iOS release extracts specific
failure modes from the generic code 1004 into dedicated codes: 1005 for no credentials
(iOS 15), 1006 for duplicate credentials (iOS 18), 1010 for device not configured (iOS
26\). On modern iOS, the most common passkey failures all have clean, dedicated signals
that require no string matching or error unwrapping.

For the [Android](https://www.corbado.com/blog/how-to-enable-passkeys-android) error code reference, see GPS
Passkey Error Codes. For the web browser error perspective, see
[WebAuthn Errors](https://www.corbado.com/blog/webauthn-errors) in Production. For setting up your analytics
framework, see [Passkey Analytics](https://www.corbado.com/blog/passkey-analytics).

## Frequently Asked Questions

### How do I silently detect 'no passkey found' on iOS without showing unnecessary UI?

Use ASAuthorizationController with preferImmediatelyAvailableCredentials set to true. If
no passkey exists for the relying party, iOS returns error code 1005 (NotInteractive)
without displaying any sheet. This approach has been available since iOS 15 and is the
recommended way to perform silent credential checks before deciding whether to prompt.

### What new passkey error codes were added in iOS 26 and what do they replace?

iOS 26 added code 1009 (PreferSignInWithApple) for when a user prefers Sign in with Apple,
and code 1010 (DeviceNotConfiguredForPasskeyCreation) for when a device lacks a passcode
or iCloud Keychain setup. Both scenarios previously surfaced as the ambiguous code 1004,
requiring string matching on localizedDescription to distinguish them.

### Why do passkey error details disappear in my Safari web app compared to a native iOS app?

Safari's WebKit layer translates ASAuthorizationError codes to DOMExceptions before
exposing them to JavaScript, losing most specificity. Only three outcomes are preserved:
code 1001 becomes NotAllowedError, code 1006 becomes InvalidStateError and security errors
become SecurityError. Codes 1005, 1009 and 1010 are native-app-only signals with no
DOMException equivalent.

### How should I handle ASAuthorizationErrorFailed (code 1004) when NSUnderlyingErrorKey is nil?

Check localizedDescription and NSLocalizedFailureReasonErrorKey first, as
NSUnderlyingErrorKey was nil for domain association failures and missing credential
scenarios in iOS 26.2 testing. NSUnderlyingErrorKey is more reliably populated for
security key operations and WebKit-originated errors. Still inspect it when present as a
fallback for those specific paths.

### How do I classify Apple passkey errors by severity for production monitoring and alerting?

Separate errors into four groups: expected outcomes (codes 1001, 1005, 1006),
environmental blockers (code 1010 and LAError -5 for no passcode), transient failures
(LAError -4 and -8) and configuration errors (code 1003 and domain association failures).
Environmental errors indicate devices where passkeys structurally cannot work and should
trigger suppression of future passkey prompts.
