Get your free and exclusive +30-page Authentication Analytics Whitepaper

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

Vincent

Created: February 9, 2026

Updated: February 10, 2026

Blog-Post-Header-Image

1. Introduction#

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).

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

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 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 vs conditional create)?
  • What minimum context should you capture so "it failed" becomes a reproducible issue?
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, manual passkey creation, conditional create and auto-triggered appends.
  • Timing from operation start is the most underrated signal: immediate (<1s), user-cancel (1-15s) and timeout (30s+) are fundamentally different categories.
  • AbortError is usually a lifecycle/concurrency issue (navigation, re-render, multiple in-flight requests).
  • SecurityError is almost always configuration/context and rare in mature production deployments.
  • "Browser error names" and "server verification rejects" are different layers. Track server rejects as explicit codes, not as generic client failures.
  • Raw error names are not actionable alone. Always capture operation type, timing, and platform context alongside error.name so you can classify errors into buckets you can actually fix.

2. A Production Cheat Sheet#

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.nameWhat it usually means in productionWhat to check to confirmFirst action (UX + engineering)
NotAllowedErrorUser 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 actionTreat as expected: restore UI + show fallback
AbortErrorYour app (or the browser) aborted the ceremonynavigation/re-render during ceremony; concurrent WebAuthn calls; AbortController.abort()Enforce one in-flight request; prevent route changes; handle abort as normal control flow
SecurityErrorContext/policy not allowedorigin + RP ID strategy; iframe/embedding; HTTPS; feature policyFix RP ID/origin configuration; validate embedding policies; ensure secure context
InvalidStateErrorState mismatch (often duplicate registration)registration vs login; excludeCredentials; existing credential on authenticatorTreat as “already enrolled”; adjust UX path; fix option generation
ConstraintErrorRequirements can’t be satisfiedauthenticatorAttachment, userVerification, resident key requirementsRelax constraints or provide alternative path/fallback. Example: Screen lock is missing on Android
DataErrorInputs are malformed/inconsistentbase64url encoding; id/challenge/user handle formatsFix encoding/serialization; add validation in option generation
NotSupportedErrorPlatform/browser doesn’t support what you askedOS/browser version; feature detection assumptionsFall back immediately; record segment; avoid showing passkey CTAs for unsupported environments
UnknownErrorPlatform/authenticator failed in a generic wayspikes after OS updates; device build; credential-manager provider issuesRetry-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:

SourceError message
Chrome, EdgeNotAllowedError: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.
Safari, WebKitNotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
Safari, WebKitNotAllowedError: This request has been cancelled by the user.
Chrome, EdgeNotAllowedError: The operation is not allowed at this time because the page does not have focus.
Safari, WebKitNotAllowedError: The document is not focused.
FirefoxNotAllowedError: 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:

SignalLikely meaningWhat to do next
Immediate failure (<1s)Environment rejection: no capability, document not focused, conditional create surface unavailableCheck feature detection; ensure document has focus; verify operation is supported on this platform
Fast cancel (1-3s)Surprise prompt / no contextChange prompt timing; add cooldown after cancel
User-length cancel (3-15s)User saw the dialog and chose not to proceedExpected UX; restore UI + show fallback
Timeout (30s+)Ceremony timed out without user actionBucket as "did not complete"; consider whether the prompt was noticed
QR/hybrid UI appears before failureNo 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/tapMissing user activationStart the ceremony from a real user gesture
During conditional create or auto-triggered appendAutofill 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.

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:

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:

SourceError message
Chrome, EdgeAbortError: The operation was aborted.
Chrome, EdgeAbortError: Aborted by AbortSignal.
FirefoxAbortError: signal is aborted without reason
FirefoxAbortError: Operation timed out.
Safari, WebKitAbortError: The user aborted a request.
ChromeAbortError: 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/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:

SourceError message
Chrome, EdgeSecurityError: WebAuthn is not supported on sites with TLS certificate errors.
Any browserSecurityError: 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.

PasskeysCheatsheet Icon

Looking for a dev-focused passkey reference? Download our Passkeys Cheat Sheet. Trusted by dev teams at Ally, Stanford CS & more.

Get Cheat Sheet

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:

SourceError message
Safari, WebKitInvalidStateError: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.
Chrome, EdgeInvalidStateError: At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.
Chrome, EdgeInvalidStateError: A request is already pending.
FirefoxInvalidStateError: 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 can detect duplicates (see excludeCredentials)
  • during conditional create, 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 (see conditional create on Chrome)

6.2 ConstraintError#

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 authenticatorAttachment or resident key 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#

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:

SourceError message
Chrome, EdgeNotSupportedError: The user agent does not support public key credentials.
FirefoxNotSupportedError: Resident credentials or empty 'allowCredentials' lists are not supported.
Chrome, Edge, FirefoxTypeError: PublicKeyCredential.parseCreationOptionsFromJSON is not a function
Chrome, Edge, FirefoxTypeError: PublicKeyCredential.parseRequestOptionsFromJSON is not a function
Chrome, Edge, FirefoxTypeError: credential.toJSON is not a function
SafariTypeError: 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, LastPass, or 1Password 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#

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:

SourceError message
Chrome (Android)UnknownError: An unknown error occurred while talking to the credential manager.
Any browserUnknownError: The operation failed for an unknown transient reason.
Any browserUnknownError: Either the device has received unexpected request data, or the device has been reconfigured since the request was made.
Any browserUnknownError: 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:

  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 (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, 1Password, etc.) is installed, configured, or set as default and react 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 - 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 two related frameworks. The error domain and code depend on whether you're creating or asserting a credential.

Passkey creation (registration):

ErrorNotes
The operation couldn't be completed. (SimpleAuthenticationServices.AuthorizationError error 1.)User dismissed the creation sheet. This is the most common iOS error overall - the NotAllowedError equivalent.
The operation couldn't be completed. (SimpleAuthenticationServices.AuthorizationError error 1.)Same message when a passkey already exists for this credential (InvalidStateError equivalent). The string is identical to user cancellation - you must distinguish by checking excludeCredentials match or flow context.

Passkey login (assertion):

ErrorNotes
The operation couldn't be completed. (com.apple.AuthenticationServices.AuthorizationError error 1001.)User cancelled the passkey login prompt (ASAuthorizationError.canceled).
The operation couldn't be completed. + localized "no credentials" messageNo passkey exists on this device for the relying party. The message after the prefix is in the user's device language (see below).
Couldn't communicate with a helper application.The credential provider extension failed to respond. Transient - retry is appropriate.
Request already in progress for specified application identifier.A duplicate ASAuthorization request was fired while one was pending. Race condition in the app.
Stolen Device Protection is enabled and biometry is required.iOS 17+ Stolen Device Protection blocks biometric auth in unfamiliar locations. Not actionable by developers, but worth surfacing to the user.
Application with identifier <TeamID.BundleID> is not associated with domain <your-domain>The app's Associated Domains entitlement doesn't match the relying party. Fix the apple-app-site-association file on your server.
(AuthenticationServicesCore.ASCABLEClient.ClientError error 2.)Cross-device authentication (hybrid/CABLE) Bluetooth handshake failed.
(AuthenticationServicesCore.ASCABLEClient.ClientError error 3.)Cross-device authentication Bluetooth connection failed.

Localized "no credentials" messages (iOS only):

When preferImmediatelyAvailableCredentials is set and no passkey exists, iOS returns a localized error 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:

LanguageMessage
Chinese (Simplified)没有可用于登录的凭证。
VietnameseKhông có sẵn thông tin để đăng nhập.
Arabicلا تتوفر بيانات اعتماد لتسجيل الدخول.
SpanishNo 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ไม่มีข้อมูลประจำตัวสำหรับเข้าสู่ระบบ
ItalianNon ci sono credenziali disponibili per l'accesso.
DutchGeen inloggegevens beschikbaar.
Japaneseログイン用の資格情報がありません。
TurkishOturum açmak için kullanılabilecek kimlik bilgisi yok.

English-locale devices typically resolve "no credentials" at the API level before the ASAuthorization framework returns a localized error, which is why no English variant appears above.

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:

ErrorNotes
User cancelled the operationUser 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 credentialA 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:

ErrorNotes
Passkeys not supported for this appDigital 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.jsonThe 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 validatedThe 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:

ErrorNotes
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 errorGeneric 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. 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’s observability SDK 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.

10.1 Client-Side Signals (Frontend SDK)#

SignalWhy it matters
error.name + normalized reason bucketRaw browser error + your classification
Operation type (conditional UI, modal login, manual create, conditional create, auto-triggered append)Same error means different things per operation
OS + browser + version + device typeSegment-specific failures
Authenticator / credential manager contextExtension and provider breakage
Time-to-error from operation startImmediate rejection (<1s) vs user cancel (1-15s) vs timeout (30s+)
Whether QR/hybrid UI appearedLocal vs cross-device failure
Correlation id (auth_flow_id)Join with server logs

10.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.

SignalWhy it matters
Challenge mismatch / expired challengeSession timing or replay issues
Origin / RP ID mismatchMulti-domain configuration bugs
Invalid signature / credential not foundDeleted 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 mismatchAccount 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.

Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

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.

11. Beyond Error Names: how Corbado turns raw Errors into actionable Signals#

The logging checklist above captures raw signals. In production at scale, error.name alone is not enough. Building this classification yourself is significant ongoing work: error messages change with every browser and OS release, password manager providers ship updates that alter ceremony behavior and new error signatures appear with every feature launch.

11.1 Why error.name alone is not enough#

The same NotAllowedError can mean six different things depending on three dimensions browsers don't separate for you:

DimensionWhat browsers give youWhat you actually needExample
Operation contextNotAllowedErrorWas it conditional UI, modal login, manual create, conditional create, or auto-append?Android returns the same "unknown error" for a login dismiss (expected) and a creation failure (unexpected)
TimingNo duration dataImmediate (<1s) vs user cancel (1-15s) vs timeout (30s+)200ms = environment rejection; 5s = user saw dialog and cancelled; 35s = timeout
Platform + authenticatorGeneric error.nameOS, browser, version, credential manager for every error"user dismissed dialog" on Chrome and "autofill unavailable" on Safari both surface as NotAllowedError

11.2 What Corbado's observability SDK captures#

This is the problem Corbado's observability SDK is built to solve. It is a lightweight frontend integration that sits on top of your existing passkey implementation, works with any WebAuthn server and any IDP, and classifies every WebAuthn error along all three dimensions automatically:

CapabilityWhat it does
Error attributionCaptures OS, OS version, browser, browser version and authenticator with every ceremony attempt
Operation modeConnects each error to the specific operation (conditional UI, modal login, manual create, conditional create, auto-append) so the same NotAllowedError resolves to different root causes
Timing from action startRecords duration from ceremony initiation to distinguish immediate rejections, user cancels and timeouts without guessing
Intelligent error classificationMatches on the full error message (not just error.name), scoped by platform and operation type. Patterns are priority-ordered and severity-classified (expected vs unexpected), and continuously updated as browsers and OS versions change.
Password manager fragmentationDetects when credential manager extensions (Bitwarden, 1Password, LastPass) intercept ceremonies and return non-standard responses, separating extension-caused failures from platform failures

This is the Observe layer: visibility into what's happening, without changing your implementation.

11.3 Two Ways to debug: Top-Down and Bottom-Up#

Corbado's management console supports two investigation paths:

Top-down (dashboard to root cause):

  1. Start with aggregate dashboards showing KPIs like Android login rate or iOS append success rate.
  2. Notice a metric dipping. Drill into the funnel for that specific segment (e.g. native app on Android).
  3. Compare against other segments (iOS, web) to confirm whether the issue is isolated or systemic.
  4. From there, jump directly into the error time series, or ask the AI Analytics Assistant to cross-correlate whether errors have increased and run z-value anomaly analysis.

Bottom-up (error patterns to impact):

  1. Start from the error classification view. Review classified error patterns and their volumes.
  2. Refine error mappings as new patterns emerge (e.g. a new browser version shipping a different error message).
  3. Errors are cross-correlated with KPI changes and surfaced as annotations on dashboards, so a spike in a specific error pattern is automatically linked to the metric it affected.

Both paths converge: top-down tells you something is wrong, bottom-up tells you why. The AI Analytics Assistant connects the two by letting you ask questions in natural language across both error data and adoption metrics.

Teams that want to act on these signals can move to Adopt, which adds passkey intelligence to automatically gate ceremonies, optimize enrollment prompts and heal broken passkey states. For regulated environments or hyperscale deployments, Enterprise adds single-tenant hosting, SIEM integration and PSD2-compliant configuration.

Enterprise Icon

Get free passkey whitepaper for enterprises.

Get for free

12. Conclusion#

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 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.

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook