[WebinarLive on Nov 18: Building a Business Case for PasskeysPasskey Business Case, recording for all registrantsRegister](https://us06web.zoom.us/webinar/register/9417924803200/WN_k47uNazUScau9dWzh2jHTA)

[Back to Overview](/content/blog/index.html)

Copy PageCopy PageCopied

🇬🇧EnEnglish

# Ultimate WebAuthn Errors in Production Guide (2026)

Learn what common WebAuthn errors like NotAllowedError mean in production and how to classify them by operation type, timing and platform context.

[Vincent Delitz](https://www.linkedin.com/in/vincent-delitz/)

Created: February 9, 2026

Updated: October 16, 2026

Table of Contents

### Enterprise Passkey Whitepaper

Practical guidance, rollout patterns and KPIs for passkey programs.

Practical guidance, rollout patterns and KPIs for passkey programs.

[Get Whitepaper](/content/passkeys/enterprise/index.html)

Share this article

* * *

))

Series: Login Failure Debugging

Passkeys Series: Login Failure Debugging

[Series overview: all 9 articles](/content/blog/series/login-failure-debugging/index.html)

[GPS Passkey Error Codes](/content/blog/google-play-services-passkey-error-codes/index.html) [Apple Passkey Error Codes](/content/blog/apple-passkey-error-codes/index.html) [Root Cause Analysis](/content/blog/authentication-failure-root-cause-analysis/index.html) [Native App Passkey Errors](/content/blog/native-app-passkey-errors/index.html) [Passkey Troubleshooting](/content/blog/passkey-troubleshooting-solutions/index.html) [Passkeys in In-App Browsers](/content/blog/passkeys-in-app-browsers/index.html) [QR Login Failure](/content/blog/qr-login-failure/index.html) [WebAuthn Errors](/content/blog/webauthn-errors/index.html) [Managed Windows Devices](/content/blog/windows-passkey-not-working-enterprise/index.html)

**Key Facts**

- `NotAllowedError` is a **surface signal**, not a root cause. It can mean cancel,
timeout, "no local credential" or missing user activation depending on context.
- **The operation type changes the meaning.** The same `NotAllowedError` means different
things during conditional UI login, modal login and conditional create.
- **Timing from operation start** is the most underrated signal: immediate (`<1s`),
user-cancel (1-15s) and timeout (30s+) are fundamentally different categories.
- **Raw error names are not actionable alone.** Capture operation type, timing and the
full environment (OS version, client version, authenticator settings, hardware model)
alongside `error.name`.
- **Login failures are P1, creation failures are P2.** Creation errors often have higher
volume, but login errors block access and need immediate alerting.

[**1\. Introduction\#**](/content/blog/webauthn-errors#1-introduction/index.html)

In production, WebAuthn errors are confusing because browsers expose a small set of
`DOMException` names (like `NotAllowedError`) that can represent multiple underlying
causes. On top of that, the vast majority of "errors" - often above 95% in optimized
[large-scale](/content/blog/introducing-passkeys-large-scale-overview/index.html) deployments - are actually
**expected behavior** (user aborted the operating system passkey prompt).

Experiment with passkey flows in the Passkeys Debugger.

[Try for Free](https://www.passkeys-debugger.io/)

Important: For privacy reasons, browsers don't distinguish whether the user actively
cancelled or whether no passkey existed. However, in some situations and with enough
context, both on the web and on native platforms, some of these cases can be
differentiated using signals like timing.

If you want the canonical definitions for these names, start with
[MDN `DOMException`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException). For
WebAuthn-specific conditions that lead to these exceptions (and what browsers are required
to enforce), see the [W3C Web Authentication spec](https://www.w3.org/TR/webauthn-3/).

If you treat all errors as “bugs”, you’ll do the wrong things:

- you’ll pollute your error metrics with normal cancels
- you’ll miss real regressions hidden inside the mass of `NotAllowedError`
- you’ll ship UI that confuses users instead of helping them recover

In this article we answer:

- What do the most common WebAuthn error names usually mean in real traffic?
- How do you disambiguate `NotAllowedError` into actionable buckets (cancel vs timeout vs
availability)?
- Why does the same error mean different things depending on the operation (conditional UI
login vs modal login vs [passkey creation](/content/blog/passkey-creation-best-practices/index.html) vs
[conditional create](/content/blog/conditional-create-passkeys/index.html))?
- What minimum context should you capture so "it failed" becomes a reproducible issue?

Recent Articles

- 👤
[Passkey Troubleshooting: Solutions for Passkey Issues & Errors](/content/blog/passkey-troubleshooting-solutions/index.html)
- ♟️
[Passkey Analytics: Track Adoption, Funnels & Login KPIs](/content/blog/passkey-analytics/index.html)
- 🔑
[Authentication Analytics: Metrics & Login Funnels](/content/blog/authentication-analytics-playbook/index.html)
- ♟️
[isUVPAA() on iOS / macOS / Android / Windows \[2026\]](/content/blog/isUserVerifyingPlatformAuthenticatorAvailable/index.html)
- 📖
[WebAuthn Conditional UI (Passkeys Autofill) Technical Explanation](/content/blog/webauthn-conditional-ui-passkeys-autofill/index.html)

[**2\. A Production Cheat Sheet\#**](/content/blog/webauthn-errors#2-a-production-cheat-sheet/index.html)

If you only need a quick mapping to unblock debugging, start with this table. It’s biased
toward what teams actually see in dashboards and support tickets.

| **`error.name`** | **What it usually means in production** | **What to check to confirm** | **First action (UX + engineering)** |
| --- | --- | --- | --- |
| `NotAllowedError` | User dismissed sheet, timed out or availability mismatch collapsed into one bucket. This is the largest error bucket in production. | time-to-error, whether QR/hybrid UI appeared, whether ceremony started from a real user action | Treat as expected: restore UI + show fallback |
| `AbortError` | Your app (or the browser) aborted the ceremony | navigation/re-render during ceremony; concurrent WebAuthn calls; `AbortController.abort()` | Enforce one in-flight request; prevent route changes; handle abort as normal control flow |
| `SecurityError` | Context/policy not allowed | origin + RP ID strategy; iframe/embedding; HTTPS; feature policy | Fix RP ID/origin configuration; validate embedding policies; ensure secure context |
| `InvalidStateError` | State mismatch (often duplicate registration) | registration vs login; `excludeCredentials`; existing credential on authenticator | Treat as “already enrolled”; adjust UX path; fix option generation |
| `ConstraintError` | Requirements can’t be satisfied | `authenticatorAttachment`, `userVerification`, resident key requirements | Relax constraints or provide alternative path/fallback. Example: Screen lock is missing on Android |
| `DataError` | Inputs are malformed/inconsistent | base64url encoding; id/challenge/user handle formats | Fix encoding/serialization; add validation in option generation |
| `NotSupportedError` | Platform/browser doesn’t support what you asked | OS/browser version; feature detection assumptions | Fall back immediately; record segment; avoid showing passkey CTAs for unsupported environments |
| `UnknownError` | Platform/authenticator failed in a generic way | spikes after OS updates; device build; credential-manager provider issues | Retry-friendly UX; capture build numbers; investigate segment spikes |

One thing that's easy to miss: the same `error.name` can mean very different things
depending on the **operation type**. Keep the operation context in mind as you read the
sections below. In practice, [passkey creation](/content/blog/passkey-creation-best-practices/index.html)
(registration) errors typically outnumber login errors by a wide margin - the table above
applies to both, but creation is where most of the volume lives.

Next, we'll go deeper on `NotAllowedError` because it's the one you'll see the most and
the one teams misinterpret most often.

[**3\. NotAllowedError: The operation either timed out or was not allowed\#**](/content/blog/webauthn-errors#3-notallowederror-the-operation-either-timed-out-or-was-not-allowed/index.html)

`NotAllowedError` often looks like "passkeys failed", but it's usually the platform
telling you the user did not complete the OS UI. The key is to split it into buckets you
can act on.

**What you'll see in the browser console:**

| **Source** | **Error message** |
| --- | --- |
| Chrome, Edge | `NotAllowedError: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.` |
| Safari, WebKit | `NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.` |
| Safari, WebKit | `NotAllowedError: This request has been cancelled by the user.` |
| Chrome, Edge | `NotAllowedError: The operation is not allowed at this time because the page does not have focus.` |
| Safari, WebKit | `NotAllowedError: The document is not focused.` |
| Firefox | `NotAllowedError: Operation failed.` |

All of these surface as `error.name === "NotAllowedError"`. The `error.message` differs by
browser engine and underlying cause, but the result is the same: the ceremony did not
complete.

This applies to both **login and passkey creation**. During login (conditional UI, modal
with or without allowList), `NotAllowedError` typically means the user didn't complete the
ceremony. During [passkey creation](/content/blog/passkey-creation-best-practices/index.html), the same error
surfaces for different reasons: the user dismissed the creation dialog (conditional create
did not work, or the page lost focus during an auto-triggered append. The operation type
changes what the error means and what you should do about it.

**Timing is often an underrated signal.** An error after less than a second of a click is
usually an immediate rejection (environment can't do it, document not focused, missing
capability). An error after a few seconds is a user cancel (they saw the dialog and
decided not to proceed). An error after 30+ seconds is a timeout. On native platforms,
timing is especially important: [authenticator](/content/glossary/authenticator/index.html) round-trips,
biometric prompts and credential manager handoffs all have characteristic durations that
help you separate "didn't work" from "user walked away". You still can't distinguish
easily if a passkey existed.

[**3.1 Disambiguate with Context\#**](/content/blog/webauthn-errors#31-disambiguate-with-context/index.html)

You don't need a perfect signal. You need enough context to avoid treating every
`NotAllowedError` the same way. [iOS](/content/blog/how-to-enable-passkeys-ios/index.html)/Safari gets
specific attention below because it has unique constraints (user activation requirements
in earlier versions), but in raw error volume, Windows and Chromium browsers often
generate more `NotAllowedError` than any other platform. These signals often get you 80%
of the way:

| **Signal** | **Likely meaning** | **What to do next** |
| --- | --- | --- |
| Immediate failure (`<1s`) | Environment rejection: no capability, document not focused, conditional create surface unavailable | Check feature detection; ensure document has focus; verify operation is supported on this platform |
| Fast cancel (1-3s) | Surprise prompt / no context | Change prompt timing; add cooldown after cancel |
| User-length cancel (3-15s) | User saw the dialog and chose not to proceed | Expected UX; restore UI + show fallback |
| Timeout (30s+) | Ceremony timed out without user action | Bucket as "did not complete"; consider whether the prompt was noticed |
| QR/hybrid UI appears before failure | No local credential available on this device. Note: reliably detecting QR code decisions before they happen requires a [passkey intelligence](https://docs.corbado.com/corbado-connect/features/passkey-intelligence) layer that knows whether a usable credential exists on the current device. | Gate passkey offers; make "Use phone" explicit; reduce surprise QR |
| Concentrated on iOS/Safari and triggered without a click/tap | Missing user activation | Start the ceremony from a real user gesture |
| During conditional create or auto-triggered append | Autofill not available, credential already exists or page lost focus. Conditional create errors can appear suddenly and at high volume when the feature is launched, making this one of the largest error sources overnight. | See conditional create; check document visibility state; use [`getClientCapabilities()`](/content/blog/webauthn-client-capabilities/index.html) to verify `conditionalCreate` support before attempting the call |

This is also why `NotAllowedError` should rarely be user-visible. It’s not a message the
user can act on.

Context classification is also where success rates split most sharply. The
[Corbado Passkey Benchmark 2026 passkey authentication success rate analysis](/content/passkey-benchmark-2026/passkey-authentication-success-rate/index.html)
measures Q1 2026 completion at 55–95% for unknown-device identifier-first flows versus
95–99% for known-device returns across
[large-scale](/content/blog/introducing-passkeys-large-scale-overview/index.html) B2C deployments.
[iOS](/content/blog/how-to-enable-passkeys-ios/index.html) web identifier- first lands at 85–95% completion
(% CDA 0–5%), Android web at 70–85% (% CDA 5–10%) and macOS web at 70–85% (% CDA 10–15%),
while Windows web sits at 45–60% identifier-first completion with % CDA at 55–65% on
[Windows 11](/content/blog/passkeys-windows-11/index.html) and 40–55% on Windows 10. Reading
`NotAllowedError` volume without separating known versus unknown device contexts conflates
two fundamentally different success regimes.

One nuance that matters in production: some user agents may surface timeouts as
`TimeoutError`, but many teams still see timeouts collapse into `NotAllowedError` in
dashboards. Either way, treat timeouts as “ceremony did not complete” and bucket using
timing plus context.

[**3.2 UX Handling: make Cancel a normal Exit\#**](/content/blog/webauthn-errors#32-ux-handling-make-cancel-a-normal-exit/index.html)

When the OS sheet is dismissed or times out, your UI should immediately recover and
[react](/content/blog/react-passkeys/index.html) gracefully. A practical set of rules:

- restore the login UI (no spinners that keep running)
- keep identifier state (don’t force re-entry)
- show a visible fallback on the same screen
- avoid scary banners for expected outcomes

Beyond the basics:

- Treat the first abort as normal and offer a retry with a calm explanation. Only after a
second abort should you suggest fallback options (see
[passkey fallback](/content/blog/passkey-fallback-recovery/index.html) and recovery).
- Allow up to three creation prompts before a cooldown so surprised users get a second
chance (see passkey creation best practices).
- Split error counts between creation and logins so you compare like with like.
- Segment error rates by OS, browser and device so you can spot where friction actually
lives (see [passkey login](/content/blog/passkey-login-best-practices/index.html) best practices).

If your “cancels” are genuinely high, the next step is to fix the root causes behind them:
prompt timing, QR surprises and low availability.

[**3.3 Engineering Fixes that reduce NotAllowedError\#**](/content/blog/webauthn-errors#33-engineering-fixes-that-reduce-notallowederror/index.html)

Start with the changes that move metrics quickly:

- Fix prompt timing and user activation (especially on
[iOS](/content/blog/how-to-enable-passkeys-ios/index.html)/Safari): Safari WebAuthn user-activated events.
- Reduce QR/hybrid surprises: why passkey flows show QR codes and users abandon.
- Gate offers so passkeys are shown when likely to succeed: offer passkeys only when they
are likely to succeed.
- Use patterns that avoid prompting when no local credential exists:
[WebAuthn immediate mediation](/content/blog/webauthn-immediate-mediation/index.html).
- **Handle Network Flakiness:** Network errors during verification often manifest as
generic client-side failures. Implement an offline queue for your telemetry logs so you
don't lose the error data when the user's connection drops during the ceremony.
- **Gate ceremonies with detection APIs** so environment failures don't inflate your
`NotAllowedError` bucket. Start with
[`isUVPAA()`](/content/blog/isUserVerifyingPlatformAuthenticatorAvailable/index.html) as the most basic
gate, then use [`getClientCapabilities()`](/content/blog/webauthn-client-capabilities/index.html) for finer
checks (conditional create, conditional get, hybrid transport,
[platform authenticator](/content/glossary/platform-authenticator/index.html)). Be aware that detection
APIs can break with OS updates: [iOS 26](/content/blog/ios-26-passkeys/index.html).2 shipped a WebKit bug
where `isUVPAA()` returns `false` on all [WKWebView](/content/blog/native-app-passkeys/index.html)-based
browsers even though passkeys work fine, causing sudden `NotAllowedError` spikes for
10-25% of iOS users (see fixing passkey detection with
[getClientCapabilities](/content/blog/webauthn-client-capabilities/index.html)()).

[**3.4 Note on evolving Error Names\#**](/content/blog/webauthn-errors#34-note-on-evolving-error-names/index.html)

Error names are a moving target. There are ongoing proposals to add more granular WebAuthn
errors (for example, to separate “no credential available” from “user cancelled”). As of
February 2026, this is not implemented in any browsers, so it’s still worth building your
own reason buckets based on context and timing. If you want to track this work, see
[WebAuthn issue #2062](https://github.com/w3c/webauthn/issues/2062) and the
["New Error Codes" explainer](https://github.com/w3c/webauthn/wiki/Explainer%3A-New-Error-Codes-%282024-Edition%29).

The remaining error names are less frequent but still worth understanding when they
appear.

[**4\. AbortError: The operation was aborted\#**](/content/blog/webauthn-errors#4-aborterror-the-operation-was-aborted/index.html)

`AbortError` is uncommon in volume compared to `NotAllowedError`, but when it appears it's
highly diagnostic: it usually means the ceremony did not finish because your app
invalidated the request - navigation happened, state changed or a second request started.

**What you'll see in the browser console:**

| **Source** | **Error message** |
| --- | --- |
| Chrome, Edge | `AbortError: The operation was aborted.` |
| Chrome, Edge | `AbortError: Aborted by AbortSignal.` |
| Firefox | `AbortError: signal is aborted without reason` |
| Firefox | `AbortError: Operation timed out.` |
| Safari, WebKit | `AbortError: The user aborted a request.` |
| Chrome | `AbortError: CredentialContainer request is not allowed.` |

Common production causes include:

- multiple concurrent WebAuthn calls (two prompts racing)
- route change/re-render during an in-flight ceremony
- calling `AbortController.abort()` during retries or state cleanup

To fix it, focus on making the ceremony a “critical section”:

- allow only one in-flight request at a time
- block navigation during the ceremony (or cancel cleanly and restore UI)
- treat abort as expected control flow: show a retry button and a fallback method

If you see `AbortError` concentrated in embedded surfaces or multi-domain apps, the next
bucket to check is `SecurityError`.

[**5\. SecurityError: WebAuthn is not supported on sites with TLS certificate errors\#**](/content/blog/webauthn-errors#5-securityerror-webauthn-is-not-supported-on-sites-with-tls-certificate-errors/index.html)

`SecurityError` is the browser telling you: "this context is not allowed to do what you
asked." In practice it's almost always configuration, not user behavior. In mature
production deployments, `SecurityError` is rare because these issues are typically caught
during integration testing. If it appears in production, it usually means a new domain,
embedding context or deployment target was added without proper WebAuthn configuration.

Common causes include:

- RP ID / origin mismatch (multi-domain setups)
- [cross-origin](/content/blog/iframe-passkeys-webauthn/index.html) embedding restrictions (iframes)
- insecure context (not HTTPS) or blocked permissions/policies
- `.well-known/webauthn` or `.well-known/assetlinks.json` misconfigured, missing or
temporarily unavailable. Network issues during the critical window when the browser
fetches these files will cause failures. A common blind spot: if your homepage is down
for maintenance, the well-known files are also offline, breaking passkey ceremonies
across all relying parties that depend on them.

**What you'll see in the browser console:**

| **Source** | **Error message** |
| --- | --- |
| Chrome, Edge | `SecurityError: WebAuthn is not supported on sites with TLS certificate errors.` |
| Any browser | `SecurityError: The relying party ID is not a registrable domain suffix of, nor equal to the current origin's effective domain.` |
| Chrome (iframe) | `SecurityError: The 'publickey-credentials-create' feature is not enabled in this document.` |

In production, `SecurityError` is rare - these are almost always caught during integration
testing. When they do appear, the TLS certificate error is the most common survivor.

The fastest debugging loop is:

- log the origin and RP ID inputs you used
- reproduce in the same context (top-level vs [iframe](/content/blog/iframe-passkeys-webauthn/index.html),
prod domain vs staging)
- if you embed login, confirm permissions policy is configured (for example
`publickey-credentials-create` / `publickey-credentials-get`):
[MDN Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy/publickey-credentials-create)
- verify your domain strategy (see
[related origins](/content/blog/webauthn-related-origins-cross-domain-passkeys/index.html))
- if you use [iframes](/content/blog/iframe-passkeys-webauthn/index.html), validate feature policies (see
[iframe](/content/blog/iframe-passkeys-webauthn/index.html) passkeys)

Once `SecurityError` is handled, the next bucket to treat seriously is the set of errors
that often indicate implementation bugs: `InvalidStateError`, `ConstraintError` and
`DataError`.

**Passkeys Cheatsheet.** Practical guidance, rollout patterns and KPIs for passkey programs.

[Get Cheat Sheet](/content/blog/passkeys-cheat-sheet/index.html)

[**6\. InvalidStateError, ConstraintError, DataError: treat as Implementation Bugs\#**](/content/blog/webauthn-errors#6-invalidstateerror-constrainterror-dataerror-treat-as-implementation-bugs/index.html)

These errors should be rare in a mature passkey implementation. When they show up, they
usually indicate that option generation is wrong for a segment or that the flow is in the
wrong state.

[**6.1 InvalidStateError: "credentials already registered with the relying party"\#**](/content/blog/webauthn-errors#61-invalidstateerror-credentials-already-registered-with-the-relying-party/index.html)

**What you'll see in the browser console:**

| **Source** | **Error message** |
| --- | --- |
| Safari, WebKit | `InvalidStateError: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.` |
| Chrome, Edge | `InvalidStateError: At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.` |
| Chrome, Edge | `InvalidStateError: A request is already pending.` |
| Firefox | `InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable` |

Typical meanings:

- registration: you attempted to create a credential that already exists (duplicate)
- login: less common; often means the flow/state is inconsistent for the platform

Practical handling:

- if it happens in manual registration, treat it as “already enrolled” and route
accordingly
- ensure `excludeCredentials` lists all existing credential IDs for the user so the
[authenticator](/content/glossary/authenticator/index.html) can detect duplicates (see
[excludeCredentials](/content/glossary/excludecredentials/index.html))
- during [conditional create](/content/blog/conditional-create-passkeys/index.html), `InvalidStateError` is
expected and should be silently ignored: it means a passkey already exists in the
provider. The same applies to `NotAllowedError` and `AbortError` during
[conditional create](/content/blog/conditional-create-passkeys/index.html) (see
[conditional create on Chrome](https://developer.chrome.com/docs/identity/webauthn-conditional-create))

[**6.2 ConstraintError\#**](/content/blog/webauthn-errors#62-constrainterror/index.html)

Typical meaning: the [authenticator](/content/glossary/authenticator/index.html) can’t satisfy your requested
constraints.

Common triggers:

- missing device screen lock (especially [Android](/content/blog/how-to-enable-passkeys-android/index.html)):
the platform requires biometric or PIN verification but the device has no lock screen
configured
- too-strict `authenticatorAttachment` or
[resident key](/content/blog/webauthn-resident-key-discoverable-credentials-passkeys/index.html)
assumptions
- hard `userVerification` requirements in segments where they're not available

Fix: relax constraints (where acceptable) or provide an alternative path. For missing
screen lock, consider detecting this condition and guiding users rather than failing
silently (Android).

[**6.3 DataError\#**](/content/blog/webauthn-errors#63-dataerror/index.html)

Typical meaning: inputs are malformed or inconsistent.

Common triggers:

- encoding mistakes (base64 vs base64url)
- invalid credential ids / challenge formatting

Fix: validate and normalize inputs at the boundary where you generate WebAuthn options. In
practice, `DataError` is effectively absent in mature production systems - if your option
generation is tested, you won't see this in dashboards.

If these errors are under control, the next question is coverage: are users failing
because the environment can’t do WebAuthn the way you expect?

[**7\. NotSupportedError: The user agent does not support public key credentials\#**](/content/blog/webauthn-errors#7-notsupportederror-the-user-agent-does-not-support-public-key-credentials/index.html)

`NotSupportedError` is a coverage signal, not a reliability signal. It usually means a
segment can't do what you asked (OS/browser too old, missing capability, feature not
enabled).

**What you'll see in the browser console:**

| **Source** | **Error message** |
| --- | --- |
| Chrome, Edge | `NotSupportedError: The user agent does not support public key credentials.` |
| Firefox | `NotSupportedError: Resident credentials or empty 'allowCredentials' lists are not supported.` |
| Chrome, Edge, Firefox | `TypeError: PublicKeyCredential.parseCreationOptionsFromJSON is not a function` |
| Chrome, Edge, Firefox | `TypeError: PublicKeyCredential.parseRequestOptionsFromJSON is not a function` |
| Chrome, Edge, Firefox | `TypeError: credential.toJSON is not a function` |
| Safari | `TypeError: Can only call PublicKeyCredential.toJSON on instances of PublicKeyCredential` |

The first two are genuine `NotSupportedError` DOMExceptions. The `TypeError` entries are
technically a different exception type but represent the same class of problem: the
browser or environment doesn't support what you asked for. The JSON serialization
`TypeError` family is far more common in practice than the `NotSupportedError`
DOMException itself (see below).

Common causes include:

- older browsers/OS versions that don't support basic WebAuthn
- requesting WebAuthn features not available on that platform
- attempting platform-specific flows on unsupported devices

**The JSON serialization family is the largest source of `NotSupportedError`-class**
**failures in production.** Technically these surface as `TypeError` (missing method), not
as a `DOMException`, but this is where you'll encounter them. Two distinct root causes:

1. **Browser doesn't support WebAuthn JSON serialization methods.** The browser has
`navigator.credentials` but not `PublicKeyCredential.parseCreationOptionsFromJSON` /
`parseRequestOptionsFromJSON`. This accounts for roughly 90% of this error family,
concentrated in older Safari and Chrome versions. If your client library depends on
these methods without a fallback, this produces significant error volume.
2. **Password manager extensions break `.toJSON()`.** Extensions like
[Bitwarden](/content/blog/passkey-analysis-bitwarden-developer-survey-2024/index.html),
[LastPass](/content/blog/lastpass-data-breach/index.html) or
[1Password](/content/blog/1password-passkeys-best-practices-analysis/index.html) can intercept the
ceremony and return an object that looks like a credential but isn't a real
`PublicKeyCredential` instance. Calling `.toJSON()` on it either throws, returns
undefined or the object is `null` entirely. This is roughly 10% of the family but
especially confusing to debug because the error messages differ by browser (Safari:
"Can only call on instances of PublicKeyCredential"; Firefox: "does not implement
interface PublicKeyCredential").

Handling should be blunt and fast:

- fall back immediately to fallback password/OTP
- record the segment so you can quantify coverage gaps
- avoid showing passkey CTAs on segments that will consistently fail

If coverage looks fine but failures still happen in specific segments, you may be dealing
with platform-layer issues surfaced as `UnknownError`.

[**8\. UnknownError: An unknown error occurred while talking to the credential manager\#**](/content/blog/webauthn-errors#8-unknownerror-an-unknown-error-occurred-while-talking-to-the-credential-manager/index.html)

`UnknownError` is a catch-all for authenticator/OS failures that don't map cleanly into
the other categories. It's often transient, but it can also spike after OS updates.

**What you'll see in the browser console:**

| **Source** | **Error message** |
| --- | --- |
| Chrome (Android) | `UnknownError: An unknown error occurred while talking to the credential manager.` |
| Any browser | `UnknownError: The operation failed for an unknown transient reason.` |
| Any browser | `UnknownError: Either the device has received unexpected request data, or the device has been reconfigured since the request was made.` |
| Any browser | `UnknownError: Something went wrong.` |
| Chrome (LastPass) | `TypeError: Cannot use 'in' operator to search for 'type' in null` |
| Safari (LastPass) | `TypeError: null is not an Object. (evaluating 'key in input')` |
| Chrome (Bitwarden) | `FallbackRequested` |

Practical handling:

- use retry-friendly UX (don't "blame the user")
- capture OS/build numbers and credential-manager/provider context where possible
- watch for segment-specific spikes after OS updates

One niche source of errors that doesn't fit neatly into any `DOMException` category:
**password manager browser extensions** (like
[Bitwarden](/content/blog/passkey-analysis-bitwarden-developer-survey-2024/index.html),
[LastPass](/content/blog/lastpass-data-breach/index.html),
[1Password](/content/blog/1password-passkeys-best-practices-analysis/index.html) and others) can intercept
WebAuthn API calls and return non-standard responses. While small in volume compared to
user cancellations, these are worth tracking because they affect specific user segments
consistently and the symptoms are confusing: missing methods on the returned credential
object, unexpected error types or malformed responses that don't match any documented
WebAuthn error. These often surface as `UnknownError` or as unclassified exceptions. If
you see error spikes concentrated in specific browsers with no OS-level explanation, check
whether a credential manager extension is involved.

So far we've covered web browser error names. But if you also ship native apps, the error
landscape is different - and in some ways, significantly better.

[**9\. A Word on native Apps (iOS and Android)\#**](/content/blog/webauthn-errors#9-a-word-on-native-apps-ios-and-android/index.html)

Everything above covers web browsers. Native apps - iOS with the ASAuthorization
framework, [Android](/content/blog/how-to-enable-passkeys-android/index.html) with Credential Manager - share
the same fundamental error categories but differ in important ways:

1. **"No credentials" is a distinct signal.** On web, browsers collapse "no credential
available" and "user cancelled" into the same `NotAllowedError` for privacy. On native,
using `preferImmediatelyAvailableCredentials` on iOS (`ASAuthorizationController`) or
`setPreferImmediatelyAvailableCredentials(true)` on
[Android](/content/blog/how-to-enable-passkeys-android/index.html) (`GetCredentialRequest`) tells the OS
to only present credentials already on the device and fail immediately if none exist.
This gives you a clean "no credentials" return that web cannot provide.

2. **Credential provider status is visible.** Native platforms in some conditions you can
tell you when no credential provider (Google Password Manager,
[iCloud Keychain](/content/glossary/icloud-keychain/index.html),
[1Password](/content/blog/1password-passkeys-best-practices-analysis/index.html), etc.) is installed,
configured or set as default and [react](/content/blog/react-passkeys/index.html) to that. On web, this
information is hidden behind opaque `NotAllowedError` messages.

3. **Error messages are more specific.** Because the user has installed the app - and
thereby established a trust relationship with the
[relying party](/content/glossary/relying-party/index.html) \- the OS surfaces more diagnostic detail. The
privacy considerations that force web browsers to be vague don't apply in the same way
when the app is already on the device. iOS returns localized messages in the user's
device language. Android returns structured error types with cause chains. This makes
debugging easier but means your error handling must account for localization and
platform-specific error formats.

[**9.1 iOS (ASAuthorization Framework)\#**](/content/blog/webauthn-errors#91-ios-asauthorization-framework/index.html)

iOS surfaces passkey errors through the ASAuthorization framework. All errors arrive in
the `authorizationController(controller:didCompleteWithError:)` delegate callback as
`NSError` objects.

**Classify by domain + code, not by message string.** The primary error domain is
`com.apple.AuthenticationServices.AuthorizationError`
(`ASAuthorizationError.errorDomain`). Cast the error with `let nsError = error as NSError`
and match on `.domain` and `.code`. Never match on `.localizedDescription` in production -
Apple's messages are localized into 30+ languages and can change across OS versions. The
message strings listed below are useful for recognizing errors in logs, but they are not
classification criteria.

**Public ASAuthorizationError codes:**

| **Code** | **Name** | **Since** | **What it means** |
| --- | --- | --- | --- |
| 1000 | `Unknown` | iOS 13 | Should not appear in production. Generic catch-all. |
| 1001 | `Canceled` | iOS 13 | User dismissed the passkey sheet. Most common error overall - the `NotAllowedError` equivalent. Clean signal with empty `userInfo` and no underlying error. |
| 1002 | `InvalidResponse` | iOS 13 | Framework-level corruption. Rare in practice. |
| 1003 | `NotHandled` | iOS 13 | No provider handled the request. Check entitlements and credential provider configuration. |
| 1004 | `Failed` | iOS 13 | Generic failure. The `localizedDescription` contains the actual reason (e.g. "Application with identifier X is not associated with domain Y"). `userInfo` may contain a `FailureReason` string, but `NSUnderlyingErrorKey` is not always populated - domain association failures return nil for the underlying error. |
| 1005 | `NotInteractive` | iOS 15 | The request ran in a context that cannot show UI (e.g. no interactive foreground scene). Predates passkeys (added in iOS 15, passkeys shipped in iOS 16) and is sometimes misdocumented as the "no credentials" signal for `preferImmediatelyAvailableCredentials` \- that case actually returns an immediate 1001 with a localized no-credentials message. |
| 1006 | `MatchedExcludedCredential` | iOS 18 | A passkey already exists for this RP on this device. Clean signal for duplicate detection - empty `userInfo`, no `NSUnderlyingErrorKey`. Classification works without string matching. |
| 1007 | `CredentialImport` | iOS 18.2 | Credential import failed. |
| 1008 | `CredentialExport` | iOS 18.2 | Credential export failed. |
| 1009 | `PreferSignInWithApple` | iOS 26 | User prefers Sign in with Apple over passkey. New in iOS 26. |
| 1010 | `DeviceNotConfiguredForPasskeyCreation` | iOS 26 | Device lacks passcode or iCloud Keychain configuration. Known iOS 26 simulator bug: returns 1010 even when configuration is correct (does not reproduce on physical devices). |

The most important distinction for production: **since iOS 18, duplicate credentials**
**return their own error code 1006 (`MatchedExcludedCredential`).** On
[iOS 17](/content/blog/apple-passkeys-integration/index.html) and earlier, duplicate credentials were hidden
inside code 1004 (`Failed`). On
[iOS 18](/content/blog/ios-18-passkeys-automatic-passkey-upgrades/index.html) +, the distinction is structural
(different error code), not textual.

**Common runtime errors (log-level reference):**

These messages appear in `localizedDescription` or in `userInfo` for specific failure
scenarios. Use them for log searches and debugging, not for programmatic classification.

| **Message (English locale)** | **Underlying code** | **Notes** |
| --- | --- | --- |
| `Application with identifier <TeamID.BundleID> is not associated with domain X` | 1004 (`Failed`) | The app's Associated Domains entitlement doesn't match the relying party. Fix the `apple-app-site-association` file on your server. |
| `Couldn't communicate with a helper application.` | 1004 (`Failed`) | The credential provider extension failed to respond. Transient - retry is appropriate. |
| `Request already in progress for specified application identifier.` | 1004 (`Failed`) | A duplicate ASAuthorization request was fired while one was pending. Race condition in the app. |
| `Stolen Device Protection is enabled and biometry is required.` | 1004 (`Failed`) | iOS 17+ Stolen Device Protection blocks biometric auth in unfamiliar locations. Not actionable by developers, but worth surfacing to the user. |
| `(AuthenticationServicesCore.ASCABLEClient.ClientError error 2.)` | Separate domain | Cross-device authentication (hybrid/CABLE) Bluetooth handshake failed. |
| `(AuthenticationServicesCore.ASCABLEClient.ClientError error 3.)` | Separate domain | Cross-device authentication Bluetooth connection failed. |

**Localized "no credentials" messages (immediate code 1001):**

When `preferImmediatelyAvailableCredentials` is set and no passkey exists, iOS returns
immediately - before any sheet is shown - with code 1001 (`Canceled`) and a localized
message in the user's device language. This is unique to native apps - web browsers never
expose this signal. The message always starts with `The operation couldn't be completed.`
followed by the localized text:

| **Language** | **Message** |
| --- | --- |
| English | `No credentials available for login.` |
| Chinese (Simplified) | `没有可用于登录的凭证。` |
| Vietnamese | `Không có sẵn thông tin để đăng nhập.` |
| Arabic | `لا تتوفر بيانات اعتماد لتسجيل الدخول.` |
| Spanish | `No hay ninguna credencial disponible para iniciar sesión.` |
| Chinese (Traditional) | `沒有可用於登入的憑證。` |
| Korean | `로그인을 위한 자격 증명이 없습니다.` |
| French (Canada) | `Aucun identifiant disponible pour la connexion.` |
| Portuguese (Brazil) | `Nenhuma credencial disponível para login.` |
| French (France) | `Aucune information d'identification n'est disponible pour procéder à la connexion.` |
| Thai | `ไม่มีข้อมูลประจำตัวสำหรับเข้าสู่ระบบ` |
| Italian | `Non ci sono credenziali disponibili per l'accesso.` |
| Dutch | `Geen inloggegevens beschikbaar.` |
| Japanese | `ログイン用の資格情報がありません。` |
| Turkish | `Oturum açmak için kullanılabilecek kimlik bilgisi yok.` |

Because the code is the same 1001 as a genuine user dismissal, these strings cannot be
your primary classifier either. Classify by request context - the flag was set and the
error returned immediately, before any sheet rendered - and use the messages above for
recognizing the case in logs.

[**9.2 Android (Credential Manager API)\#**](/content/blog/webauthn-errors#92-android-credential-manager-api/index.html)

Android surfaces passkey errors through the Credential Manager API
(`androidx.credentials`). Error messages include a primary message and often a `cause`
with additional detail. Compared to iOS, Android provides more structured error types and
more explicit causes for configuration issues.

**User cancellation and credential detection:**

| **Error** | **Notes** |
| --- | --- |
| `User cancelled the operation` | User dismissed the passkey prompt. The `NotAllowedError` equivalent. Note: the Credential Manager also returns `User canceled the request` (US spelling) from a different code path - both are identical. |
| `Excluded credential matches existing credential` | A passkey already exists for this credential ID. The `InvalidStateError` equivalent. Unlike iOS, the message is distinct from user cancellation. |
| `No create options available.` | No eligible credential provider can handle the creation request. Typically means Google Play Services is outdated or no credential provider supports passkey creation. |

**Configuration and security errors:**

| **Error** | **Notes** |
| --- | --- |
| `Passkeys not supported for this app` | Digital Asset Links (`assetlinks.json`) is missing or does not contain the app's signing certificate fingerprint. The `SecurityError` equivalent. |
| `Https failed: respCode=301, url=https://<domain>/.well-known/assetlinks.json` | The `assetlinks.json` file returns a redirect instead of HTTP 200. Android requires the file at the exact URL without redirects. |
| `The incoming request cannot be validated` | The Credential Manager cannot verify the request against Digital Asset Links. |
| `RP ID cannot be validated.` | The relying party ID in the WebAuthn options does not match `assetlinks.json`. |
| `Screen lock is missing.` | No PIN, pattern or biometric configured on the device. Passkeys require user verification. The `ConstraintError` equivalent. |
| `Cannot find an eligible account.` | No Google account on the device is eligible for passkey creation (rare, typically custom enterprise setups). |

**Platform and authenticator errors:**

| **Error** | **Notes** |
| --- | --- |
| `Unsuccessful result from folsom activity.` | Google Play Services internal failure. "Folsom" is a GMS component for passkey operations. Transient - retry is appropriate. |
| `Can't find the proper key to decrypt the private key from WebauthnCredentialSpecifics.` | A synced passkey exists but the device cannot decrypt its private key. The Google Password Manager sync state is inconsistent - the credential was synced from another device but the decryption key is unavailable. Not actionable by developers. |
| `Operation was interrupted` (cause: `The UI was interrupted - please try again.`) | The Credential Manager UI was interrupted by another activity (incoming call, screen rotation, app backgrounded). The `AbortError` equivalent. |
| `Unknown credential error` | Generic catch-all when no specific error type applies. Typically transient. |
| `timeout` (cause: `Canceled`) | The Credential Manager operation timed out before the user completed biometric verification. |

[**10\. Putting it all together: the Error Taxonomy\#**](/content/blog/webauthn-errors#10-putting-it-all-together-the-error-taxonomy/index.html)

The following diagram shows how all the layers discussed above - Infrastructure,
Environment, Operation type, Classification and Detection - connect end to end. It is the
mental model you should have in mind when designing your error tracking.

Error Analysis & Runtime

The Environment (Client Side)

Infrastructure (Server/Network)

Mitigation Actions

Detection Goals

Layer 5: Modality & Complexity

Layer 4: WebAuthn Operation

Layer 3: Client Software

Layer 2: OS & Version

Layer 1: Platform

Manifests as

Manifests as

Server Side Error

500 / Logic Error

Network Error

Timeout / 400 Bad Request

Client-Side Generic Error

Web / Browser

Native / App

OS: Windows, macOS, Linux

OS: iOS, Android

OS Version / Settings

(Screen Lock, Biometrics)

Browser: Chrome, Safari, Edge

\+ Version

Native App / WebView

\+ App Version

Operation Type

Login

(navigator.credentials.get)

Registration

(navigator.credentials.create)

CUI / Conditional UI

Automatic Operation

CDA / Cross-Device Auth

Classification

Expected Error

(User Aborted Ceremony)

Unexpected Error

(System/Crash/Unknown)

P1: Login Problems

P2: Creation Problems

Detect Baseline Drift

(Increased Expected)

Detect New Anomalies

(Increased Unexpected)

Deploy Hotfix / New Version

Activate Automatic Fallback

(Disable Passkey)

Click to enlarge and copy chart definition

The key insight: a raw `error.name` only makes sense once you know which layer of the
Environment produced it, which Operation was running and whether the error was Expected or
Unexpected. The sections below cover what to log and how to act on it.

[**11\. What to log so Errors become debuggable\#**](/content/blog/webauthn-errors#11-what-to-log-so-errors-become-debuggable/index.html)

Most of the error classification in this article can be done with client-side signals
alone. A frontend-only observability SDK captures enough context to classify the vast
majority of WebAuthn errors. This is also how Corbado Observe is architected: the
client-side layer handles error attribution, timing, operation context and platform
detection. Server-side logging adds a second layer for failures that only the backend can
see.

The key requirement: every attempt must be joinable end-to-end. A shared correlation id
(e.g. `auth_flow_id`) connects client-side context with the server verification outcome.

[**11.1 Client-Side Signals (Frontend SDK)\#**](/content/blog/webauthn-errors#111-client-side-signals-frontend-sdk/index.html)

| **Signal** | **Why it matters** |
| --- | --- |
| `error.name` \+ normalized reason bucket | Raw browser error + your classification |
| Operation type | Conditional UI, modal login, manual create, conditional create, auto-append. **CDA (Cross-Device Auth)** is its own complex beast. |
| Full Environment Context | OS + Version, Browser + Version, **Hardware Brand/Model**, **Authenticator Settings** (e.g., GPM enabled?) |
| Authenticator / credential manager context | Extension and provider breakage |
| Time-to-error from operation start | Immediate rejection (`<1s`) vs user cancel (1-15s) vs timeout (30s+) |
| Network / Connectivity Status | Network errors often manifest as client errors. Track if the user was offline and **queue logs** to send when connectivity restores. |
| Whether QR/hybrid UI appeared | Local vs cross-device failure |
| Correlation id (`auth_flow_id`) | Join with server logs |

[**11.2 Server-Side Signals (Backend Verification)\#**](/content/blog/webauthn-errors#112-server-side-signals-backend-verification/index.html)

Server verification failures happen after the browser returns a credential and signed
challenge. These should be structured errors with explicit codes, not mixed into the same
bucket as client-side `DOMException` names. See:
[WebAuthn server implementation](/content/blog/webauthn-server-implementation/index.html).

| **Signal** | **Why it matters** |
| --- | --- |
| Challenge mismatch / expired challenge | Session timing or replay issues |
| Origin / RP ID mismatch | Multi-domain configuration bugs |
| Invalid signature / credential not found | Deleted or corrupted credential. Common case: conditional UI login with a passkey the user already deleted server-side. Use the Signal API to keep client and server credential lists in sync. |
| User handle mismatch | Account mapping issues |
| Correlation id (`auth_flow_id`) | Join with client-side context |

If you want a full funnel model (drop-offs by step and conversion between steps), see:
passkey telemetry to understand drop-offs.

Subscribe to our Passkeys Substack for the latest news.

[Subscribe](https://passkeys.substack.com/)

Passkey Funnel Analysis: Process Mining for Authentication \| Corbado Connect - YouTube

Tap to unmute

[Passkey Funnel Analysis: Process Mining for Authentication \| Corbado Connect](https://www.youtube.com/watch?v=wnrXJzvBjsU) [Corbado](https://www.youtube.com/channel/UCtfk8jvZhJpe6P-0FlzkC_w)

Corbado181 subscribers

With this data in place, the conclusion becomes simple: most “errors” become either UX
fixes, coverage fixes or configuration fixes. But building and maintaining this
classification yourself is significant ongoing work.

[**12\. How Corbado can help\#**](/content/blog/webauthn-errors#12-how-corbado-can-help/index.html)

Everything above comes down to one problem: `error.name` alone cannot tell you whether a
failure was a user cancel, a timeout, an environment rejection or a real regression.
Building that classification in-house means chasing browser and OS message changes
forever. [Corbado Observe](/content/observe/index.html) is the
[authentication observability](/content/blog/authentication-observability/index.html) layer that does this
for you. It is a frontend integration that sits on top of your existing passkey stack,
works with any WebAuthn server and any IDP and classifies every ceremony automatically.

[Look's like your browser doesn't support this video.See Passkey Errors in Corbado Observe →](/content/observe/passkey-errors/index.html) [**12.1 Why `error.name` alone is not enough\#**](/content/blog/webauthn-errors#121-why-/index.html)

The same `NotAllowedError` can mean several different things depending on three dimensions
browsers never separate for you. Observe reconstructs all three from every ceremony.

| **Dimension** | **What browsers give you** | **What you actually need** | **Example** |
| --- | --- | --- | --- |
| **Operation context** | `NotAllowedError` | Whether it was conditional UI, modal login, manual create, conditional create or an auto-append | Android returns the same "unknown error" for a login dismiss (expected) and a creation failure (unexpected) |
| **Timing** | No duration data | Immediate (`<1s`) vs user cancel (1-15s) vs timeout (30s+) | 200ms means environment rejection, 5s means the user saw the dialog and cancelled, 35s means timeout |
| **Platform + authenticator** | Generic `error.name` | OS, browser, version and credential manager for every error | "user dismissed dialog" on Chrome and "autofill unavailable" on Safari both surface as `NotAllowedError` |

[**12.2 What Corbado Observe captures\#**](/content/blog/webauthn-errors#122-what-corbado-observe-captures/index.html)

Observe is a lightweight frontend integration that sits on top of your existing passkey
implementation and classifies every WebAuthn error along all three dimensions. Each
capability maps to a view in the Observe console.

| **Capability** | **Observe view** | **What it does** |
| --- | --- | --- |
| **Error attribution** | [Passkey Errors](/content/observe/passkey-errors/index.html) | Records OS, OS version, browser, browser version and authenticator with every ceremony, so a spike resolves to a concrete segment instead of a raw `NotAllowedError` count |
| **Operation and timing** | [Subflow Explorer](/content/observe/subflow-explorer/index.html) | Ties each error to its operation and to the time-to-error from ceremony start, with p50 and p90 timing per step so immediate rejections, user cancels and timeouts separate cleanly |
| **Error classification** | [Passkey Errors](/content/observe/passkey-errors/index.html) | Matches on full error context beyond the raw name, ranks patterns by OS, browser and login method and splits expected user aborts from unexpected system failures |
| **Capability coverage** | Client Capabilities, Technology | Shows which environments can run conditional UI, conditional create and platform authenticators, so `NotSupportedError`-class gaps map to concrete segments |
| **Anomaly detection** | Markets, Touchpoints | Flags baseline drift (a slow rise in expected cancels after an OS update) and sudden spikes (a new browser version or IDP change) and cross-correlates them with the KPI they moved |

The result is visibility into what is happening, without changing your implementation.

[**12.3 Two ways to investigate: top-down and bottom-up\#**](/content/blog/webauthn-errors#123-two-ways-to-investigate-top-down-and-bottom-up/index.html)

The Observe console supports two investigation paths that converge on the same root cause.

**Top-down (dashboard to root cause):**

1. **Watch for two anomaly types.** Baseline drift is a gradual rise in user cancels for a
specific environment (for example
[iOS 18](/content/blog/ios-18-passkeys-automatic-passkey-upgrades/index.html).2 on iPhone 15) after an OS
update. Spikes are a new error or a sudden jump in failures that points to a breaking
change or a regression in a new browser version.

2. **Prioritize by impact.** Treat a drop in login success as urgent and keep
creation-side problems on trend watch, so a user-cancel spike during enrollment does
not outrank a broken sign-in.

3. **Drill into the environment.** Use the granular dimensions (hardware, authenticator
settings) to tell whether the issue is global or specific to
[Samsung](/content/blog/samsung-passkeys/index.html) devices on Android 14.

**Bottom-up (error patterns to impact):**

1. Start from the Passkey Errors view and review the classified patterns and their
volumes.
2. Refine error mappings as new patterns appear, for example a new browser version
shipping a different message for the same condition.
3. Read the [KPI annotations](/content/observe/annotations/index.html): patterns are cross-correlated with
metric changes and surfaced on dashboards, so a spike in one pattern links to the
metric it moved.

Both paths converge: top-down tells you something is wrong, bottom-up tells you why.

You keep your current passkey implementation and get the classified, segmented error view
this article argues for, without building and maintaining the pattern library yourself.

Get free passkey whitepaper for enterprises.

[Get for free](/content/passkeys/enterprise/index.html)

[**13\. Conclusion\#**](/content/blog/webauthn-errors#13-conclusion/index.html)

WebAuthn error names are not a verdict. They're hints - and they only become actionable
when you connect them to the operation type, the timing and the platform context.

- **What do the most common WebAuthn error names mean in production?** Most map to a small
set of layers: user control flow (`NotAllowedError`), app lifecycle/concurrency
(`AbortError`), security context/config (`SecurityError`) or option/state bugs
(`InvalidStateError`, `ConstraintError`, `DataError`). The vast majority of volume is
`NotAllowedError`, and most of that is expected behavior (user dismissed the prompt).
- **How do you disambiguate `NotAllowedError`?** Use timing (immediate rejection vs user
cancel vs timeout), a QR/hybrid indicator (availability mismatch), user-activation
context (especially on iOS/Safari) and the operation type (conditional UI vs modal login
vs passkey creation vs conditional create). Don't treat all `NotAllowedError` as one
failure mode.
- **Why does the operation type matter?** The same `error.name` during a
[conditional UI](/content/glossary/conditional-ui/index.html) login is a completely different signal than
during a conditional create or a manual passkey creation (user dismissed dialog).
Logging the operation type alongside the error is what turns generic `NotAllowedError`
into an actionable bucket.
- **What minimum context makes errors debuggable?** Capture `error.name`, operation type,
time-to-error from operation start, flow type, whether QR/hybrid UI was shown,
OS/browser/device (including versions), a correlation id (`auth_flow_id`) and server
verification rejects as explicit codes.

Two rules of thumb that cut across all error types: never show raw browser errors to
users - always provide a clear fallback path - and separate local attempts from QR/hybrid
cross-device attempts, because they fail for different reasons and need different fixes.
At scale, maintaining error classification across browsers, OS versions and credential
managers is ongoing work. Consider using an observability SDK with a maintained pattern
library rather than building this from scratch.

## About Corbado

Corbado is the **Passkey Intelligence Platform** for large-scale CIAM teams running consumer authentication. We help you see what IDP logs and generic analytics tools can't: where passkeys, passwords, OTP, social login and fallback journeys succeed, stall or fail, which devices and browsers create friction, and when an OS update silently breaks login. Two products: **Corbado Observe** layers **process mining and observability across authentication journeys.** **Corbado Connect** adds **managed passkeys with analytics built in** alongside your IDP. VicRoads runs passkeys for 5M+ users with Corbado (more than 80% passkey activation rate). [Talk to a Passkey Expert →](/content/contact/index.html)

[**Frequently Asked Questions\#**](/content/blog/webauthn-errors#frequently-asked-questions/index.html) [**How do I tell the difference between a user cancelling a passkey prompt and the device not having a passkey?\#**](/content/blog/webauthn-errors#how-do-i-tell-the-difference-between-a-user-cancelling-a-passkey-prompt-and-the-device-not-having-a-passkey/index.html)

On web, browsers collapse both cases into `NotAllowedError` for privacy reasons, so you
cannot distinguish them directly. Use timing as a proxy: an error under 1 second typically
means environment rejection or missing capability, while 1-15 seconds suggests the user
saw and dismissed the dialog. On native iOS and Android apps, setting
`preferImmediatelyAvailableCredentials` before the request gives you a clean "no
credentials" signal before any UI is shown (on iOS an immediate code 1001 return with a
localized no-credentials message, on Android via `GetCredentialRequest`).

[**Why is my WebAuthn error rate so high even though passkeys seem to be working for most users?\#**](/content/blog/webauthn-errors#why-is-my-webauthn-error-rate-so-high-even-though-passkeys-seem-to-be-working-for-most-users/index.html)

In optimized large-scale passkey deployments, over 95% of recorded WebAuthn errors are
expected user aborts, not system failures. Tracking raw `error.name` counts without
separating "user dismissed prompt" from genuine failures inflates error metrics and masks
real regressions hidden inside the `NotAllowedError` volume. Split your counts by
operation type and treat user-cancel buckets separately from unexpected errors like
`SecurityError`, `ConstraintError` and `DataError`.

[**How do I detect "no passkey on this device" on iOS with preferImmediatelyAvailableCredentials?\#**](/content/blog/webauthn-errors#how-do-i-detect-no-passkey-on-this-device-on-ios-with-preferimmediatelyavailablecredentials/index.html)

When `preferImmediatelyAvailableCredentials` is set on `ASAuthorizationController` and no
passkey exists on the device, iOS returns immediately - before any sheet is shown - with
code 1001 (`Canceled`) and a localized no-credentials message (English: "No credentials
available for login."). There is no dedicated error code: a genuine user dismissal returns
the same 1001, so classify by the flag being set plus the immediate return before any UI
rendered, and treat the message string only as confirmation because Apple localizes it
into 30+ languages. Code 1005 (`NotInteractive`) is a different signal - the request ran
in a context that cannot show UI.

[**How should I handle NotAllowedError during a conditional create or auto-triggered passkey append flow?\#**](/content/blog/webauthn-errors#how-should-i-handle-notallowederror-during-a-conditional-create-or-auto-triggered-passkey-append-flow/index.html)

During conditional create, `NotAllowedError`, `AbortError` and `InvalidStateError` are all
expected outcomes and should be silently ignored rather than surfaced as errors.
`NotAllowedError` indicates autofill is unavailable or the page lost focus, while
`InvalidStateError` means a passkey already exists in the provider. Before attempting the
call, use `getClientCapabilities()` to verify `conditionalCreate` support and check
document visibility state to avoid inflating your error counts.

[**What context should I log alongside error.name to make WebAuthn failures actually debuggable?\#**](/content/blog/webauthn-errors#what-context-should-i-log-alongside-errorname-to-make-webauthn-failures-actually-debuggable/index.html)

Capture operation type (conditional UI, modal login, manual create, conditional create or
auto-append), time-to-error from operation start, OS version, browser version, hardware
model, whether QR/hybrid UI appeared and a correlation ID to join with server-side logs.
Server verification failures such as challenge mismatch, invalid signature and credential
not found should be logged as explicit structured codes, not mixed into the same bucket as
client-side `DOMException` names. These signals together allow classification of the vast
majority of errors into actionable buckets without guessing.

See how Corbado fits your passkey rollout and existing authentication stack.

[Explore the Console](https://app.corbado.com/#signup-init)

Share this article

* * *

))

Related Articles

\\
\\
**WebAuthn Immediate Mediation explained** \\
\\
Vincent Delitz - June 20, 2025](/content/blog/webauthn-immediate-mediation/index.html)

\\
\\
**9 WebAuthn Server Implementation Libraries Compared** \\
\\
Nicolai - December 15, 2023](/content/blog/webauthn-server-implementation/index.html)

\\
\\
**WebAuthn Server Options: Overview of Early Adopters** \\
\\
Vincent Delitz - July 9, 2024](/content/blog/webauthn-server-options-overview/index.html)

\\
\\
**WebAuthn Passkey QR Codes & Bluetooth: Hybrid Transport** \\
\\
Vincent Delitz - November 8, 2023](/content/blog/webauthn-passkey-qr-code/index.html)

\\
\\
**Passkeys & iframes: How to Create & Login with a Passkey?** \\
\\
Vincent Delitz - June 3, 2024](/content/blog/iframe-passkeys-webauthn/index.html)

\\
\\
**Safari WebAuthn User Gesture Requirements Not Detected** \\
\\
Vincent Delitz - May 14, 2024](/content/blog/safari-webauthn-user-activated-events/index.html)

Previous slideNext slide

Table of Contents
