---
title: Error variants
description: Every CredentialError variant, its message, its miette diagnostic code, and exactly what produces it.
tags:
- reference
- errors
---
# Error variants
Every fallible operation in the crate returns `CredentialError`. It
derives `Debug`, `Clone`, `Error` (via `thiserror`) and `Diagnostic`
(via `miette`), and is `#[non_exhaustive]`.
## The five variants
| `NotFound { name: String }` | `credential not found: {name}` | `rtb::credentials::not_found` |
| `LiteralRefusedInCi` | `literal credential is refused in CI environments` | `rtb::credentials::literal_refused` |
| `Keychain(String)` | `keychain backend error: {0}` | `rtb::credentials::keychain` |
| `ReadOnly` | `this credential store is read-only` | `rtb::credentials::read_only` |
| `Io(Arc<std::io::Error>)` | `I/O error: {0}` | `rtb::credentials::io` |
Because the enum is `#[non_exhaustive]`, a downstream `match` needs a
wildcard arm, and new variants can be added without a major version
bump.
## `NotFound`: nothing in the chain produced a value
Returned by `Resolver::resolve` when every configured leg missed, and by
`MemoryStore`, `EnvStore` and `KeyringStore` when the requested entry is
absent.
The `name` field is a diagnostic label, not a machine-readable key. What
it contains depends on the producer:
| `MemoryStore` / `KeyringStore` | `"{service}/{account}"` |
| `EnvStore` | the environment variable name |
| `Resolver::resolve` | `fallback_env`, else `env`, else `"{service}/{account}"`, else `<unnamed credential>` |
`NotFound` from a store during the resolver's keychain leg is not
surfaced to the caller. The resolver treats it as a miss and continues
down the chain.
## `LiteralRefusedInCi`: a literal secret was declined under CI
Returned by `Resolver::resolve` when `cref.literal` is set and the `CI`
environment variable is exactly `"true"`.
The variant carries no payload, and the resolver returns it **instead of
continuing** to the `fallback_env` leg.
Its `miette` help text is:
```text
set CI=false locally, or move the secret to a keychain/env var
```
This is a policy refusal, not a technical failure. The secret was
present and readable; the crate declined to hand it over.
## `Keychain(String)`: the backend failed, or a lock was poisoned
Wraps a message from the platform keychain. Produced by `KeyringStore`
for any `keyring` error other than "no entry", including a failure to
construct the entry, and for a `spawn_blocking` join failure (the
latter formatted as `join error: {e}`).
`MemoryStore` also returns this variant, with the message
`in-memory lock poisoned`, when its `RwLock` has been poisoned by a
panic in another thread. There is no separate variant for that case, so
matching on `Keychain` does not prove a keychain was involved.
The inner `String` is a rendered message. It is not structured and
should not be parsed to distinguish causes.
## `ReadOnly`: the store does not support mutation
Returned by `EnvStore::set`, `EnvStore::delete`, `LiteralStore::set` and
`LiteralStore::delete`.
It signals a permanent property of the store, not a transient condition
or a permissions problem. Retrying, or running with more privilege,
will not change the outcome. `MemoryStore` and `KeyringStore` never
return it.
## `Io`: reserved, and never produced by this crate
`Io` exists and converts from `std::io::Error` through a `From` impl:
```rust
impl From<std::io::Error> for CredentialError {
fn from(e: std::io::Error) -> Self {
Self::Io(Arc::new(e))
}
}
```
**No built-in store returns it.** Nothing in the crate constructs `Io`
outside that conversion, so matching on it while using only the four
supplied stores matches a branch that cannot currently be reached.
It is there for downstream `CredentialStore` implementations: a
file-backed or network-backed store can use `?` on an I/O operation and
get the conversion for free.
## Why the enum is `Clone` when `io::Error` is not
`std::io::Error` does not implement `Clone`, which would normally make
any enum containing it non-`Clone`. The `Io` variant wraps its error in
an `Arc` specifically to avoid that.
The reason is fan-out. One credential failure often needs to reach
several consumers at once (a log line, a telemetry event, a
health-check aggregate), and a `Clone`-able error lets each take a copy
instead of the producer having to rebuild an equivalent error per
consumer or hand out references with awkward lifetimes.
The cost is that cloning an `Io` error shares one underlying
`io::Error` rather than duplicating it. Since the error is only ever
read, that is invisible in practice.
## Rendering an error to a user
`CredentialError` implements `miette::Diagnostic`, so a tool whose `main`
returns `miette::Result` gets the code and help text rendered without
extra work. The `miette` dependency is declared with the `fancy` feature,
which pulls in the graphical report renderer.
```rust
fn main() -> miette::Result<()> {
// a CredentialError propagated with `?` renders with its
// rtb::credentials::* code and any help text attached
Ok(())
}
```
Only `LiteralRefusedInCi` carries help text. The other four variants
have a diagnostic code but no `help`, so they render as the message and
code alone.