LayerZeroFault
passkey recovery

Fix: WebAuthn 'AbortError: The user operation was aborted' in Passkey Conditional UI

VV

Written by

Fact-Checked on September 11, 2026

Verified Expert

Fix: WebAuthn “AbortError: The user operation was aborted” in Passkey Conditional UI

In modern Web3 authentication and Account Abstraction workflows—where decentralized applications use passkeys (WebAuthn) for gasless onboarding and biometric session signing—Conditional UI (Autofill) offers the gold standard user experience. When configured properly, users see biometric passkey suggestions directly inside browser input fields.

However, developers integrating embedded signers (such as Privy, Turnkey, or ZeroDev) frequently report a frustrating, intermittent exception in production:

DOMException: The user operation was aborted.
  name: "AbortError"
  code: 20
  at navigator.credentials.get (auth.ts:184)
  at handlePasskeySignIn (SignInModal.tsx:92)

In many cases, this error not only aborts the current transaction or login request, but on iOS Safari and macOS Chrome, it triggers an internal rate-limit lockout where the browser refuses to display the system biometric prompt (Touch ID / Face ID) until the user hard-refreshes the webpage.

Placeholder: WebAuthn Conditional UI Autofill Prompt Mismatch Diagram


1. W3C WebAuthn Specification: The Single-Active-Request Rule

Under Section 5.1 of the W3C Web Authentication Level 2 & Level 3 Specifications, a user agent (browser) is prohibited from executing concurrent public key credential ceremonies:

“If another credential creation or retrieval request is pending within the current browsing context or across contexts that share an authenticator session, the user agent MUST either abort the pending request or reject the new request with an AbortError or NotAllowedError.”

When implementing Passkey Conditional UI, developers mount the credential listener on page load:

// Background conditional request mounted in useEffect()
navigator.credentials.get({
  publicKey: getPublicKeyOptions,
  mediation: 'conditional', // Waits silently for user to tap an input autofill prompt
  signal: abortController.signal,
});

The bug manifests when the user bypasses autofill and explicitly clicks a “Connect with Passkey” button. The click handler initiates a standard modal ceremony:

// User-initiated modal prompt
await navigator.credentials.get({
  publicKey: getPublicKeyOptions,
  // mediation defaults to 'optional'
});

Because the background conditional request was still listening for input, the browser encounters two conflicting requests on the same origin. Chrome, Safari, and Firefox handle this by abruptly killing one or both ceremonies with AbortError.


2. Platform-Specific Edge Cases

Edge Case 1: WebKit Input Field Blur / Virtual DOM Re-renders

On Safari (iOS 17+ and macOS Sonoma/Sequoia), WebKit ties conditional mediation directly to the DOM state of <input autocomplete="webauthn">.

  • If your UI dynamically switches tabs or re-renders the input element (common in React useState triggers), WebKit detects that the anchor input has detached.
  • WebKit immediately aborts the credentials promise with error code 20.

Edge Case 2: Unchecked AbortController Lifecycle

If developers attempt to cancel the background listener by calling abortController.abort(), the pending navigator.credentials.get() promise rejects with AbortError. If the code lacks a catch block that filters out deliberate aborts, the unhandled rejection bubbles up to global error telemetry (Sentry, Datadog) as a critical authentication crash.

Edge Case 3: Resident Key Enumeration Delay on Android / Windows Hello

When resident credentials (discoverable keys) exceed internal secure enclave memory, Android Credential Manager and Windows Hello TPM require up to 800ms to enumerate keys. If an impatient user taps “Cancel” or double-clicks the sign-in button, the hardware aborts with AbortError and leaves the TPM thread stalled.

(For hardware TPM resident key management, see our deep-dive on WebAuthn Resident Key Discoverable Credential Overflow Fix).

Placeholder: Sequence Diagram of Conditional UI to Modal Passkey Cancellation Flow


3. The Robust Passkey Request Manager Pattern

To eliminate AbortError and browser prompt freezing, applications must serialize all WebAuthn calls through a Singleton Request Manager with active abort orchestration:

// auth/PasskeyManager.ts
export class PasskeyManager {
  private static instance: PasskeyManager;
  private currentAbortController: AbortController | null = null;
  private isRequestPending: boolean = false;

  private constructor() {}

  public static getInstance(): PasskeyManager {
    if (!PasskeyManager.instance) {
      PasskeyManager.instance = new PasskeyManager();
    }
    return PasskeyManager.instance;
  }

  /**
   * Safely aborts any existing WebAuthn operation before proceeding.
   */
  public async cancelPendingRequest(reason = 'New ceremony requested'): Promise<void> {
    if (this.currentAbortController && this.isRequestPending) {
      this.currentAbortController.abort(reason);
      this.currentAbortController = null;
      // Yield to the event loop so the browser engine can finalize teardown
      await new Promise((resolve) => setTimeout(resolve, 50));
    }
  }

  /**
   * Initiates Conditional UI autofill safely.
   */
  public async startConditionalAutofill(
    options: CredentialRequestOptions,
    onSuccess: (cred: Credential | null) => void
  ): Promise<void> {
    // 1. Verify browser support for conditional mediation
    if (
      typeof window.PublicKeyCredential === 'undefined' ||
      !window.PublicKeyCredential.isConditionalMediationAvailable
    ) {
      return;
    }

    const isAvailable = await window.PublicKeyCredential.isConditionalMediationAvailable();
    if (!isAvailable) return;

    await this.cancelPendingRequest('Starting conditional autofill');

    const controller = new AbortController();
    this.currentAbortController = controller;
    this.isRequestPending = true;

    try {
      const credential = await navigator.credentials.get({
        ...options,
        mediation: 'conditional',
        signal: controller.signal,
      });

      if (credential) {
        onSuccess(credential);
      }
    } catch (err: unknown) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        // Expected behavior when cleanly cancelled by user action or modal switch
        return;
      }
      console.warn('Conditional autofill error:', err);
    } finally {
      if (this.currentAbortController === controller) {
        this.isRequestPending = false;
        this.currentAbortController = null;
      }
    }
  }

  /**
   * Initiates explicit modal passkey ceremony (e.g., button click).
   */
  public async requestModalPasskey(
    options: CredentialRequestOptions
  ): Promise<Credential | null> {
    // 1. Abort any existing conditional autofill first!
    await this.cancelPendingRequest('User triggered modal authentication');

    const controller = new AbortController();
    this.currentAbortController = controller;
    this.isRequestPending = true;

    try {
      const credential = await navigator.credentials.get({
        ...options,
        mediation: 'optional',
        signal: controller.signal,
      });
      return credential;
    } catch (err: unknown) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        throw new Error('Passkey ceremony cancelled by user.');
      }
      throw err;
    } finally {
      this.isRequestPending = false;
      this.currentAbortController = null;
    }
  }
}

4. Frontend Integration Checklist

1. Correct Input Attributes for Autofill

Ensure your HTML input tag specifies the exact autocomplete properties:

<input
  type="text"
  id="username"
  name="username"
  autocomplete="username webauthn"
  placeholder="Enter wallet address or ENS"
/>

Note: The token webauthn MUST be included in the autocomplete attribute for WebKit and Blink to activate conditional mediation.

2. Component Teardown Cleanup

In React or Vue components, always cancel the abort controller in the unmount lifecycle:

useEffect(() => {
  const manager = PasskeyManager.getInstance();
  manager.startConditionalAutofill(requestOptions, (credential) => {
    handleAuthentication(credential);
  });

  return () => {
    manager.cancelPendingRequest('Component unmounted');
  };
}, []);

Placeholder: Clean React Architecture with PasskeyManager Teardown Diagram


5. Verification Matrix Across Modern Browsers

EnvironmentSupported MediationConcurrency BehaviorFix Verification
Safari (iOS 17+)conditional, optionalThrows AbortError immediately if input unmounts or modal overlaps.PasskeyManager.cancelPendingRequest() prevents prompt freezing.
Chrome / Brave (Desktop)conditional, optionalQueues requests or aborts earlier listener with error code 20.Clean AbortController filtering eliminates unhandled promise rejections.
Firefox (Desktop 122+)optional (Full conditional in preview)Rejects concurrent get() with NotAllowedError.Pre-check validation via isConditionalMediationAvailable() skips unsupported browsers.
Partner Spotlight: Gate.io

Trade Securely on Gate.io

Don't risk your assets on centralized silos or unverified endpoints. Trade securely on Gate.io with deep liquidity and institutional-grade security protocols.

Claim $100 Sign-up Bonus

Official Partner Referral Link

Related Inquiries

Why does navigator.credentials.get() throw 'AbortError: The user operation was aborted'?

The W3C WebAuthn specification enforces a strict single-active-request rule per browser window context. If a dApp initiates a background passkey autofill query using mediation: 'conditional', and the user subsequently clicks a 'Sign In with Passkey' button that calls navigator.credentials.get() without aborting the background promise, the browser immediately aborts the active operation and throws a DOMException AbortError.

How does Safari on iOS and macOS handle WebAuthn conditional mediation differently than Chrome?

Safari and WebKit strictly bind conditional mediation sessions to active form input focus. If a user taps outside the input field, if the DOM element loses focus, or if client-side frameworks (e.g., React or Next.js) re-render the input with a new virtual DOM node while WebAuthn is pending, WebKit dispatches an automatic AbortError and locks subsequent biometric prompts for up to 30 seconds.

Can an AbortController safely cancel background autofill requests without breaking modal passkey prompts?

Yes. Best practice requires creating an AbortController instance dedicated to the conditional request. Before triggering a modal passkey prompt (via button click), developers must call controller.abort('modal_override') and await the microtask resolution of the aborted promise before calling navigator.credentials.get() with standard modal mediation.