Learn what common WebAuthn errors like NotAllowedError mean in production and how to classify them by operation type, timing and platform context.
Vincent
Created: February 9, 2026
Updated: February 10, 2026

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).
Want to experiment with passkey flows? Try our 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:
NotAllowedErrorIn this article we answer:
NotAllowedError into actionable buckets (cancel vs timeout vs
availability)?Recent Articles
NotAllowedError is a surface signal, not a root cause. It can mean cancel,
timeout, "no local credential", or missing user activation depending on context.NotAllowedError means different
things during conditional UI login, modal login, manual
passkey creation, conditional create and
auto-triggered appends.<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.error.name so you can classify errors into buckets you can
actually fix.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.
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.
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.
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.
When the OS sheet is dismissed or times out, your UI should immediately recover and react gracefully. A practical set of rules:
Beyond the basics:
If your “cancels” are genuinely high, the next step is to fix the root causes behind them: prompt timing, QR surprises and low availability.
Start with the changes that move metrics quickly:
NotAllowedError bucket. Start with
isUVPAA() as the most basic
gate, then use getClientCapabilities() 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
where isUVPAA() returns false on all WKWebView-based
browsers even though passkeys work fine, causing sudden NotAllowedError spikes for
10-25% of iOS users (see
fixing passkey detection with getClientCapabilities()).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.
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:
AbortController.abort() during retries or state cleanupTo fix it, focus on making the ceremony a “critical section”:
If you see AbortError concentrated in embedded surfaces or multi-domain apps, the next
bucket to check is SecurityError.
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:
.well-known/webauthn or .well-known/assetlinks.json misconfigured, missing, or
temporarily unavailable. Network issues during the critical window when the browser
fetches these files will cause failures. A common blind spot: if your homepage is down
for maintenance, the well-known files are also offline, breaking passkey ceremonies
across all relying parties that depend on them.What you'll see in the browser console:
| Source | Error message |
|---|---|
| Chrome, Edge | SecurityError: WebAuthn is not supported on sites with TLS certificate errors. |
| Any browser | SecurityError: The relying party ID is not a registrable domain suffix of, nor equal to the current origin's effective domain. |
| Chrome (iframe) | SecurityError: The 'publickey-credentials-create' feature is not enabled in this document. |
In production, SecurityError is rare - these are almost always caught during integration
testing. When they do appear, the TLS certificate error is the most common survivor.
The fastest debugging loop is:
publickey-credentials-create / publickey-credentials-get):
MDN Permissions-PolicyOnce SecurityError is handled, the next bucket to treat seriously is the set of errors
that often indicate implementation bugs: InvalidStateError, ConstraintError and
DataError.

Looking for a dev-focused passkey reference? Download our Passkeys Cheat Sheet. Trusted by dev teams at Ally, Stanford CS & more.
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.
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:
Practical handling:
excludeCredentials lists all existing credential IDs for the user so the
authenticator can detect duplicates (see
excludeCredentials)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)Typical meaning: the authenticator can’t satisfy your requested constraints.
Common triggers:
authenticatorAttachment or
resident key
assumptionsuserVerification requirements in segments where they're not availableFix: 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:
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?
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:
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:
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..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:
If coverage looks fine but failures still happen in specific segments, you may be dealing
with platform-layer issues surfaced as UnknownError.
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:
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.
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 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.
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.
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.
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):
| Error | Notes |
|---|---|
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):
| Error | Notes |
|---|---|
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" message | No 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:
| Language | Message |
|---|---|
| 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. |
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.
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. |
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.
| 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-triggered append) | Same error means different things per operation |
| OS + browser + version + device type | Segment-specific failures |
| 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+) |
| Whether QR/hybrid UI appeared | Local vs cross-device failure |
Correlation id (auth_flow_id) | Join with server logs |
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.
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.
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.
error.name alone is not enough#The same NotAllowedError can mean six different things depending on three dimensions
browsers don't separate for you:
| Dimension | What browsers give you | What you actually need | Example |
|---|---|---|---|
| Operation context | NotAllowedError | Was 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) |
| Timing | No duration data | Immediate (<1s) vs user cancel (1-15s) vs timeout (30s+) | 200ms = environment rejection; 5s = user saw dialog and cancelled; 35s = timeout |
| Platform + authenticator | Generic error.name | OS, browser, version, credential manager for every error | "user dismissed dialog" on Chrome and "autofill unavailable" on Safari both surface as NotAllowedError |
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:
| Capability | What it does |
|---|---|
| Error attribution | Captures OS, OS version, browser, browser version and authenticator with every ceremony attempt |
| Operation mode | Connects 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 start | Records duration from ceremony initiation to distinguish immediate rejections, user cancels and timeouts without guessing |
| Intelligent error classification | Matches 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 fragmentation | Detects 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.
Corbado's management console supports two investigation paths:
Top-down (dashboard to root cause):
Bottom-up (error patterns to impact):
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.
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.
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).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.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.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.
Related Articles
Table of Contents