# Error taxonomy
> **TARGET architecture — implementation pending.** This document is the
> authoritative target error contract. The signatures are design direction,
> not the current scaffold ABI.
## Root error
Normal facade methods expose one root with two recovery categories:
```rust,ignore
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CaptchaError {
#[error(transparent)]
Configuration(#[from] ConfigurationError),
#[error(transparent)]
Verification(#[from] VerificationError),
}
```
The variant is `Verification`, never `VerificationError`. A new root variant is
justified only when callers need a materially different recovery or action
category—not merely because a provider returned a new code. There is no
`CaptchaError::ProviderRejected`: a provider-declared client-token failure is
`Ok(ProviderVerdict::Rejected { .. })`.
`CaptchaBuilder::build`, `Captcha::verify`, `Captcha::assess`, and
`ProviderAdapter::verify` return `Result<_, CaptchaError>`. Narrow standalone
constructors and parsers may retain their narrow error type when callers are
already operating below the facade boundary.
## Configuration errors
`ConfigurationError` means verification cannot be trusted until configuration,
credentials, account state, or policy changes:
```rust,ignore
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigurationError {
#[error("invalid provider configuration for {provider}")]
InvalidProviderConfiguration { provider: ProviderId },
#[error("provider credentials were rejected for {provider}")]
ProviderCredentialsRejected { provider: ProviderId },
#[error("invalid verification policy")]
InvalidPolicy,
#[error("{provider} does not support required capability {capability:?}")]
UnsupportedCapability {
provider: ProviderId,
capability: Capability,
},
}
```
Credentials, sitekey, account status, or equivalent provider codes belong here
even when they can only be discovered during a remote `verify` call. Discovery
time does not turn a configuration problem into a transient verification
failure. Implementations may add safe typed sources to variants when they
preserve this category and the redaction rules below.
## Verification errors
`VerificationError` means this attempt did not obtain a trustworthy provider
verdict:
```rust,ignore
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TokenState {
NotSent,
PossiblyConsumed,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum VerificationError {
#[error("transport failure while contacting {provider}")]
Transport {
provider: ProviderId,
#[source]
source: TransportError,
token_state: TokenState,
},
#[error("verification timed out for {provider}")]
Timeout {
provider: ProviderId,
token_state: TokenState,
},
#[error("provider unavailable: {provider}")]
ProviderUnavailable {
provider: ProviderId,
retry_after: Option<Duration>,
token_state: TokenState,
},
#[error("invalid provider protocol response from {provider}")]
Protocol {
provider: ProviderId,
#[source]
source: ProtocolError,
token_state: TokenState,
},
}
impl VerificationError {
pub fn token_state(&self) -> TokenState;
}
```
`NotSent` is returned only when the SDK can prove the token did not cross a
possible consumption boundary. Any ambiguity—including timeout, cancellation
after send as tracked internally, partial write, or a response that cannot be interpreted—maps to
`PossiblyConsumed`. A protocol or availability error may report `NotSent` only
when the official exchange proves the token was not processed. Configuration
errors happen before an attempt and do not have token state. See
[Attempts and safe retries](attempts-and-retries.md).
Rate limiting maps to `ProviderUnavailable { retry_after }` unless a future API
can demonstrate materially distinct caller semantics that justify a separate
variant. It does not justify a speculative root category.
`VerificationError::is_transient()` and `retry_after()` may help an application
choose its outage behavior. They are **not permission to replay a token**. The
API does not expose `is_retryable()`: only the adapter can decide whether a
bounded retry is safe under the provider's single-use and idempotency rules.
## Protocol errors
`ProtocolError` gives typed, bounded reasons without retaining raw bodies:
```rust,ignore
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProtocolError {
#[error("provider response exceeded the configured size limit")]
ResponseTooLarge,
#[error("provider response was malformed")]
MalformedResponse,
#[error("provider response omitted a required field")]
MissingRequiredField,
#[error("provider response contained an invalid field")]
InvalidField,
#[error("provider response version or shape is unsupported")]
UnsupportedResponse,
#[error("provider returned an unexpected HTTP status")]
UnexpectedStatus,
}
```
Implementation may add safe structured details, such as a closed normalized
field identifier or status class. It must not retain a raw body, token, secret,
credential-bearing URL, or arbitrary provider string.
## Provider-code mapping
Every adapter maps provider outcomes into exactly one of three buckets:
| Client token is invalid, expired, malformed, duplicated, or already used | `Ok(ProviderVerdict::Rejected { .. })` with a normalized `ProviderRejectionReason` | Challenge the client again or deny |
| Credentials, sitekey, account, key type, or provider configuration is invalid | `Err(CaptchaError::Configuration(..))` | Fix operator configuration; do not blame the client |
| Outage, rate limit, transport failure, timeout, or malformed/unsupported protocol | `Err(CaptchaError::Verification(..))` | Apply explicit outage policy; adapter alone controls safe retry |
Unknown provider codes fail closed into the closest safe non-accepting bucket.
They never become `Ok(Accepted)` and never cause provider rejection to be added
as a root error.
## Traits, sources, and redaction
All public error types implement [`std::error::Error`](https://doc.rust-lang.org/std/error/trait.Error.html),
`Debug`, `Display`, `Send`, `Sync`, and `'static`. Public error enums are
[`#[non_exhaustive]`](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute).
The root uses transparent `#[from]` wrapping; leaf variants use `#[source]`
where preserving a safe causal chain helps diagnosis. This follows the Rust
[error interoperability guideline](https://rust-lang.github.io/api-guidelines/interoperability.html#c-good-err)
and can be implemented with [`thiserror`](https://docs.rs/thiserror/latest/thiserror/).
`Debug`, `Display`, and source chains must redact tokens, secrets, raw provider
bodies, sensitive URLs, and arbitrary upstream messages that may contain them.
Transport wrappers normalize unsafe library errors before exposing a source.
## Stable serialized codes
Error objects themselves implement neither `Serialize` nor `Deserialize`.
Serialization uses a separate safe projection:
```rust,ignore
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CaptchaErrorCode {
InvalidProviderConfiguration,
ProviderCredentialsRejected,
InvalidPolicy,
UnsupportedCapability,
Transport,
Timeout,
ProviderUnavailable,
ProtocolResponseTooLarge,
ProtocolMalformedResponse,
ProtocolMissingRequiredField,
ProtocolInvalidField,
ProtocolUnsupportedResponse,
ProtocolUnexpectedStatus,
}
```
`CaptchaError::code()` may return this projection. Codes have stable
snake_case serialization and contain no messages, URLs, payloads, sources,
tokens, secrets, or provider-supplied arbitrary strings. The code is telemetry
and API data, not a deserialized error or authorization artifact.