Skip to main content

Error Handling

Every failure in webauthn-server-buildkit is a typed WebAuthnError subclass that carries a stable, machine-readable code and a suggested HTTP statusCode. Branch on the code — never on the human-readable message, which may change.

The error classes

All errors extend WebAuthnError:

ClassDefault statusCodeThrown for
WebAuthnError400Base class — catch this to handle everything.
RegistrationError400Registration ceremony failures.
AuthenticationError401Authentication ceremony failures.
VerificationError400Generic verification failures (challenge, client data).
ConfigurationError500Invalid configuration at construction time.
StorageError500Storage adapter problems.
SessionError401Session token failures.
import { WebAuthnError } from 'webauthn-server-buildkit';

try {
await webauthn.verifyAuthentication(assertion, challenge, credential);
} catch (err) {
if (err instanceof WebAuthnError) {
res.status(err.statusCode).json({ error: err.code });
} else {
res.status(500).json({ error: 'INTERNAL' });
}
}

Because each instance has a statusCode, you can map failures to HTTP responses generically without a big switch — 400 for bad input and most verification failures, 401 for auth/session failures, 500 for configuration/storage problems.

Branching on code

code is typed as an open union (WebAuthnErrorCode | (string & {})), so known codes autocomplete while custom/future codes still compile.

if (err instanceof WebAuthnError) {
switch (err.code) {
case 'CHALLENGE_EXPIRED':
case 'CHALLENGE_NOT_FOUND':
return promptRetry();
case 'COUNTER_ERROR':
return flagPossibleClone();
case 'ALGORITHM_MISMATCH':
return rejectHard();
default:
return genericFailure();
}
}

Notable error codes by category

CategoryCodes
Challenge lifecycleCHALLENGE_NOT_FOUND, CHALLENGE_OPERATION_MISMATCH, CHALLENGE_EXPIRED, CHALLENGE_MISMATCH
Algorithm / COSEALGORITHM_MISMATCH, UNSUPPORTED_ALGORITHM, UNSUPPORTED_CURVE, COSE_ALG_MISMATCH, COSE_DECODE_ERROR
Client data / originINVALID_CLIENT_DATA_TYPE, ORIGIN_MISMATCH, RPID_MISMATCH, CROSS_ORIGIN_NOT_ALLOWED
Authenticator dataUSER_PRESENCE_REQUIRED, USER_VERIFICATION_REQUIRED, AUTHENTICATOR_DATA_TOO_SHORT
RegistrationREGISTRATION_VERIFICATION_FAILED, CREDENTIAL_ALREADY_REGISTERED, ATTESTATION_SIGNATURE_INVALID
Attestation trustATTESTATION_INVALID, ATTESTATION_CHAIN_INVALID, ATTESTATION_NOT_TRUSTED, ATTESTATION_UNSUPPORTED_FORMAT
AuthenticationAUTHENTICATION_VERIFICATION_FAILED, CREDENTIAL_ID_MISMATCH, USER_HANDLE_MISMATCH, COUNTER_ERROR, SIGNATURE_VERIFICATION_FAILED
SessionSESSION_EXPIRED, SESSION_NOT_FOUND, INVALID_TOKEN, TOKEN_CREATION_FAILED
Config / storageCONFIGURATION_ERROR, STORAGE_ERROR, STORAGE_REQUIRED

See the full list in the API reference.

Frequently asked questions

Should I show error codes to end users?

No — map them to friendly messages. Codes are for your logs and control flow. For example, present CHALLENGE_EXPIRED as "Your sign-in attempt timed out, please try again," and COUNTER_ERROR as a security alert you investigate.

Why match on code instead of the message?

Messages are human-readable and may be reworded between versions; codes are a stable contract. Branching on code keeps your error handling resilient to copy changes.

Which errors mean "let the user retry" vs "block"?

Lifecycle errors like CHALLENGE_EXPIRED / CHALLENGE_NOT_FOUND are retryable — issue a fresh challenge. Security errors like COUNTER_ERROR, ALGORITHM_MISMATCH, and ATTESTATION_NOT_TRUSTED indicate something is wrong with the credential or environment and should generally be blocked and investigated.