Fixing InvalidStateError on Registration
InvalidStateError thrown from navigator.credentials.create() looks alarming but is usually good news: it means the authenticator already holds a credential that matches your excludeCredentials list — the user is trying to enrol a device they already registered. The spec deliberately reports this generically to avoid leaking which credential matched, so you infer “already registered” from the fact that you sent the exclude list. This page shows how to handle that case as a benign outcome, and how to distinguish it from the rarer overlapping-ceremony cause. It sits under Debugging WebAuthn Protocol Errors.
Cause Signatures
| Context | Meaning | Correct handling |
|---|---|---|
create() with non-empty excludeCredentials |
Authenticator already holds an excluded credential | Treat as “already enrolled” → route to sign-in |
create() with empty excludeCredentials |
Rare; authenticator-specific state issue | Retry once; log for investigation |
Overlapping create()/get() in flight |
A prior ceremony was not aborted | Abort prior ceremony; retry |
The excludeCredentials mechanism (WebAuthn L2 §5.4.3) is what makes the first row by far the most common. It exists precisely to stop double-registration on one authenticator.
Root Cause Analysis
1. excludeCredentials matched (the common case). You correctly populated the exclude list with the user’s existing credential IDs; the authenticator recognised one of its own and refused to create a duplicate, throwing InvalidStateError. This is the system working as designed.
2. Overlapping ceremony. A pending conditional get() or a prior create() was not aborted before this call, so two ceremonies collide. This surfaces as InvalidStateError too — distinguish it by checking whether a ceremony was already in flight.
3. Stale exclude list omitting a synced credential. If a synced passkey exists on another device but your exclude list is built from a stale query, the user may partially double-register; keep the list current.
Step-by-Step Resolution
Step 1 — Populate excludeCredentials correctly
const options = await generateRegistrationOptions({
rpID: 'example.com',
userID: account.userHandle,
userName: account.username,
excludeCredentials: account.credentials.map((c) => ({
id: c.credentialId,
type: 'public-key',
transports: c.transports,
})),
});
Step 2 — Treat the matched case as success
try {
const credential = await navigator.credentials.create({ publicKey: decode(options), signal: newCeremony() });
await registerCredential(credential as PublicKeyCredential);
} catch (err) {
const name = (err as DOMException).name;
if (name === 'InvalidStateError') {
// Already enrolled on this authenticator — not a failure
showInfo('This device already has a passkey for your account.');
routeToSignIn();
return;
}
if (name === 'AbortError') return;
throw err;
}
Step 3 — Rule out an overlapping ceremony
Ensure a single AbortController guards every ceremony (the pattern from resolving timeouts and AbortError), so InvalidStateError can be confidently attributed to excludeCredentials.
Step 4 — Keep the exclude list fresh
Rebuild excludeCredentials from a live query of the user’s credentials at each registration, including synced ones.
Verification and Testing
Reproduce deterministically with the DevTools WebAuthn virtual authenticator:
1. Register a credential on the virtual authenticator.
2. Call create() again with that credentialId in excludeCredentials.
⇒ InvalidStateError (expected "already enrolled")
Assert that your handler routes the InvalidStateError path to sign-in and does not render an error component. Add a test that an empty-excludeCredentials registration succeeds so you can distinguish the two causes.
Pitfalls
1. Showing an error for “already enrolled”. Route to sign-in instead; it is a benign outcome.
2. Empty excludeCredentials on re-registration. Populate it, or users double-register and you get duplicate credentials.
3. Blaming the authenticator for an overlap. Abort prior ceremonies first so the cause is unambiguous.
Related
- Debugging WebAuthn Protocol Errors — the parent triage reference and flag decoding
- Resolving WebAuthn SecurityError (RP ID and Origin) — the sibling registration/authentication error class
- Designing Secure Registration Endpoints — server-side
excludeCredentialsassembly and attestation handling