Ultimate WebAuthn Errors in Production Guide (2026)
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.
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.
Share this article
))
Series: Login Failure Debugging
Passkeys Series: Login Failure Debugging
Series overview: all 9 articles
GPS Passkey Error Codes Apple Passkey Error Codes Root Cause Analysis Native App Passkey Errors Passkey Troubleshooting Passkeys in In-App Browsers QR Login Failure WebAuthn Errors Managed Windows Devices
Key Facts
NotAllowedErroris 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
NotAllowedErrormeans 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.
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 deployments - are actually
expected behavior (user aborted the operating system passkey prompt).
Experiment with passkey flows in the Passkeys Debugger.
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. For
WebAuthn-specific conditions that lead to these exceptions (and what browsers are required
to enforce), see the W3C Web Authentication spec.
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
NotAllowedErrorinto 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 vs conditional create)?
- What minimum context should you capture so "it failed" becomes a reproducible issue?
Recent Articles
- 👤 Passkey Troubleshooting: Solutions for Passkey Issues & Errors
- ♟️ Passkey Analytics: Track Adoption, Funnels & Login KPIs
- 🔑 Authentication Analytics: Metrics & Login Funnels
- ♟️ isUVPAA() on iOS / macOS / Android / Windows [2026]
- 📖 WebAuthn Conditional UI (Passkeys Autofill) Technical Explanation
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
(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#
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, 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 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#
You don't need a perfect signal. You need enough context to avoid treating every
NotAllowedError the same way. iOS/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 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() 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
measures Q1 2026 completion at 55–95% for unknown-device identifier-first flows versus
95–99% for known-device returns across
large-scale B2C deployments.
iOS 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 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#
When the OS sheet is dismissed or times out, your UI should immediately recover and react 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 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 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#
Start with the changes that move metrics quickly:
- Fix prompt timing and user activation (especially on iOS/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.
- 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
NotAllowedErrorbucket. Start withisUVPAA()as the most basic gate, then usegetClientCapabilities()for finer checks (conditional create, conditional get, hybrid transport, platform authenticator). Be aware that detection APIs can break with OS updates: iOS 26.2 shipped a WebKit bug whereisUVPAA()returnsfalseon all WKWebView-based browsers even though passkeys work fine, causing suddenNotAllowedErrorspikes for 10-25% of iOS users (see fixing passkey detection with getClientCapabilities()).
3.4 Note on evolving Error Names#
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 and the "New Error Codes" explainer.
The remaining error names are less frequent but still worth understanding when they appear.
4. AbortError: The operation was aborted#
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#
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 embedding restrictions (iframes)
- insecure context (not HTTPS) or blocked permissions/policies
.well-known/webauthnor.well-known/assetlinks.jsonmisconfigured, 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, prod domain vs staging)
- if you embed login, confirm permissions policy is configured (for example
publickey-credentials-create/publickey-credentials-get): MDN Permissions-Policy - verify your domain strategy (see related origins)
- if you use iframes, validate feature policies (see iframe 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.
6. InvalidStateError, ConstraintError, DataError: treat as Implementation Bugs#
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"#
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
excludeCredentialslists all existing credential IDs for the user so the authenticator can detect duplicates (see excludeCredentials) - during conditional create,
InvalidStateErroris expected and should be silently ignored: it means a passkey already exists in the provider. The same applies toNotAllowedErrorandAbortErrorduring conditional create (see conditional create on Chrome)
Typical meaning: the authenticator can’t satisfy your requested constraints.
Common triggers:
- missing device screen lock (especially Android): the platform requires biometric or PIN verification but the device has no lock screen configured
- too-strict
authenticatorAttachmentor resident key assumptions - hard
userVerificationrequirements 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).
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#
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:
- Browser doesn't support WebAuthn JSON serialization methods. The browser has
navigator.credentialsbut notPublicKeyCredential.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. - Password manager extensions break
.toJSON(). Extensions like Bitwarden, LastPass or 1Password can intercept the ceremony and return an object that looks like a credential but isn't a realPublicKeyCredentialinstance. Calling.toJSON()on it either throws, returns undefined or the object isnullentirely. 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#
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,
LastPass,
1Password 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)#
Everything above covers web browsers. Native apps - iOS with the ASAuthorization framework, Android with Credential Manager - share the same fundamental error categories but differ in important ways:
"No credentials" is a distinct signal. On web, browsers collapse "no credential available" and "user cancelled" into the same
NotAllowedErrorfor privacy. On native, usingpreferImmediatelyAvailableCredentialson iOS (ASAuthorizationController) orsetPreferImmediatelyAvailableCredentials(true)on Android (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.Credential provider status is visible. Native platforms in some conditions you can tell you when no credential provider (Google Password Manager, iCloud Keychain, 1Password, etc.) is installed, configured or set as default and react to that. On web, this information is hidden behind opaque
NotAllowedErrormessages.Error messages are more specific. Because the user has installed the app - and thereby established a trust relationship with the relying party - 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)#
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 and earlier, duplicate credentials were hidden
inside code 1004 (Failed). On
iOS 18 +, 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)#
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#
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#
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)#
| 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)#
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.
| 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.
Passkey Funnel Analysis: Process Mining for Authentication | Corbado Connect - YouTube
Tap to unmute
Passkey Funnel Analysis: Process Mining for Authentication | Corbado Connect Corbado
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.
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 is the
authentication observability 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 → 12.1 Why error.name alone is not enough#
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#
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 | 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 | 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 | 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#
The Observe console supports two investigation paths that converge on the same root cause.
Top-down (dashboard to root cause):
Watch for two anomaly types. Baseline drift is a gradual rise in user cancels for a specific environment (for example iOS 18.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.
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.
Drill into the environment. Use the granular dimensions (hardware, authenticator settings) to tell whether the issue is global or specific to Samsung devices on Android 14.
Bottom-up (error patterns to impact):
- Start from the Passkey Errors view and review the classified patterns and their volumes.
- Refine error mappings as new patterns appear, for example a new browser version shipping a different message for the same condition.
- Read the KPI annotations: 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.
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 isNotAllowedError, 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 allNotAllowedErroras one failure mode. - Why does the operation type matter? The same
error.nameduring a conditional UI 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 genericNotAllowedErrorinto 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 →
Frequently Asked Questions# How do I tell the difference between a user cancelling a passkey prompt and the device not having a passkey?#
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?#
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?#
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.
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?#
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.
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