Supakeys

Error Handling

All passkey operations return a result object with success, error, and data fields. This page covers error codes and handling strategies.

Error Structure

interface PasskeyError {
  code: PasskeyErrorCode
  message: string
  cause?: unknown // Original error, if any
}

Error Codes

CodeDescription
NOT_SUPPORTEDWebAuthn not available in browser/context
INVALID_INPUTInvalid input provided (e.g., bad email)
CANCELLEDUser cancelled the operation
TIMEOUTOperation timed out (default: 60s)
INVALID_STATEPasskey already exists on device
SECURITY_ERRORBlocked by security policy
CHALLENGE_EXPIREDChallenge TTL exceeded (5 min)
CHALLENGE_MISMATCHChallenge validation failed
VERIFICATION_FAILEDCredential verification failed
CREDENTIAL_NOT_FOUNDPasskey doesn't exist
USER_NOT_FOUNDUser account not found
CREDENTIAL_EXISTSPasskey already registered
RATE_LIMITEDToo many requests
NETWORK_ERRORNetwork request failed
UNKNOWN_ERRORUnexpected error

Helper Functions

createError()

Create a passkey error manually.

import { createError } from 'supakeys'

const error = createError('CANCELLED', 'User cancelled registration')

getErrorMessage()

Get a user-friendly message for an error code.

import { getErrorMessage } from 'supakeys'

const message = getErrorMessage('RATE_LIMITED')
// "Too many attempts. Please wait a moment and try again."

isPasskeyError()

Type guard to check if an error is a PasskeyError.

import { isPasskeyError } from 'supakeys'

try {
  // ...
} catch (error) {
  if (isPasskeyError(error)) {
    console.log(error.code, error.message)
  }
}

mapWebAuthnError()

Convert native WebAuthn errors to PasskeyError.

import { mapWebAuthnError } from 'supakeys'

try {
  await navigator.credentials.create(options)
} catch (error) {
  const passkeyError = mapWebAuthnError(error)
  console.log(passkeyError.code) // e.g., 'CANCELLED'
}

Handling Patterns

Basic Error Handling

const result = await passkeys.register({ email })

if (!result.success) {
  switch (result.error.code) {
    case 'CANCELLED':
      // User closed the dialog
      break
    case 'INVALID_STATE':
      // Passkey already exists
      showError('You already have a passkey on this device')
      break
    case 'RATE_LIMITED':
      showError('Too many attempts. Please wait.')
      break
    default:
      showError(result.error.message)
  }
  return
}

// Success

User-Friendly Messages

import { getErrorMessage } from 'supakeys'

const result = await passkeys.signIn()

if (!result.success) {
  const userMessage = getErrorMessage(result.error.code)
  toast.error(userMessage)
}

Retry Logic

async function signInWithRetry(maxAttempts = 3) {
  for (let i = 0; i < maxAttempts; i++) {
    const result = await passkeys.signIn()

    if (result.success) {
      return result
    }

    // Don't retry user cancellation
    if (result.error.code === 'CANCELLED') {
      return result
    }

    // Don't retry rate limiting
    if (result.error.code === 'RATE_LIMITED') {
      await sleep(5000) // Wait 5 seconds
      continue
    }

    // Retry network errors
    if (result.error.code === 'NETWORK_ERROR') {
      await sleep(1000)
      continue
    }

    return result
  }
}

Error Logging

const result = await passkeys.register({ email })

if (!result.success) {
  // Log for debugging
  console.error('Passkey registration failed:', {
    code: result.error.code,
    message: result.error.message,
    cause: result.error.cause,
  })

  // Show user-friendly message
  showError(getErrorMessage(result.error.code))
}

Error Recovery

INVALID_STATE (Duplicate Passkey)

if (result.error.code === 'INVALID_STATE') {
  // Already have a passkey on this device
  const confirm = await showConfirm('You already have a passkey on this device. Sign in instead?')
  if (confirm) {
    return passkeys.signIn({ email })
  }
}

CHALLENGE_EXPIRED

if (result.error.code === 'CHALLENGE_EXPIRED') {
  // Start a new registration/login flow
  showMessage('Session expired. Please try again.')
  return passkeys.register({ email })
}

NOT_SUPPORTED

if (result.error.code === 'NOT_SUPPORTED') {
  // Fall back to password auth
  redirectTo('/login/password')
}

On this page