LayerZeroFault
passkey recovery Updated:

WebAuthn Conditional Mediation AbortError & Autofill Lock Fix

VV

Written by

Updated:

Technical Guide

The browser exception DOMException: The operation was aborted (AbortError) during WebAuthn passkey login triggers when a modal authentication call collides with an active conditional UI (autofill) request. Browsers permit only one pending navigator.credentials.get() ceremony per browsing context. Without explicitly aborting the conditional listener using an AbortController.abort() signal before initiating a modal prompt, the ceremony locks and throws an AbortError.

To test your browser’s platform authenticator parameters and WebAuthn client support offline, use our interactive WebAuthn Passkey Debugger Tool. If your users encounter mobile biometric lockouts, see our Android Credential Manager NotAllowedError guide.


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

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');
  };
}, []);

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.
Interactive Diagnostic Sandbox

Local WebAuthn & Smart Account Sandboxes

Debug WebAuthn credential requests, PRF extensions, and simulate ERC-4337 passkey account validation in client WebCrypto.

Companion Diagnostic Guide Android Credential Manager WebAuthn NotAllowedError & Lock Fix →

Fix Android Credential Manager NotAllowedError and 'Authentication needed: Enter password or unlock'. Resolve biometric enclaves and assetlinks.json.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

Why does WebAuthn throw 'AbortError: The operation was aborted'?

An AbortError triggers when an ongoing navigator.credentials.get() request is cancelled before completion. This frequently happens when an existing conditional UI (autofill) request is not cleanly cancelled via AbortController before triggering a modal passkey prompt.

How do I cancel a pending conditional UI request cleanly?

Create an instance of AbortController and pass its signal into the conditional credentials.get({ mediation: 'conditional', signal }) call. When the user clicks a modal login button, call controller.abort() before starting the modal ceremony.

Can multiple passkey authentication ceremonies run simultaneously?

No. The WebAuthn specification strictly forbids concurrent credential requests in the same browsing context. Any new call made while another is active results in an immediate AbortError or InvalidStateError.

How can I verify WebAuthn clientDataJSON and mediation support?

Use our interactive WebAuthn Passkey Debugger tool to inspect browser platform authenticator capabilities and client parameters.