Error Types
Every fallible operation in Kova returns Result<_, KovaError>. Library code
never panics — if something can fail, it’s in the Result. KovaError is a
single flat enum (thiserror-derived), so you match on exactly the case you
care about.
use kova_sdk::error::KovaError;The variants
Section titled “The variants”| Variant | Raised by | Retryable? |
|---|---|---|
Provider { message, status_code, class } | Model API returned an error response | if class is RateLimited/Overloaded |
Connection(String) | Network unreachable / DNS / TLS; MCP transport-level I/O failures | ✅ |
Timeout(Duration) | A request exceeded its timeout | ✅ |
ToolExecution { tool_name, message } | A tool returned Err (not an in-band error) | ❌ |
ToolNotFound(String) | Model called a tool that isn’t registered | ❌ |
Mcp(String) | A server-reported MCP (JSON-RPC) protocol error | ❌ |
Stream(String) | Streaming error | ❌ |
MaxIterations(usize) | Agentic loop exceeded max_iterations | ❌ |
Cancelled | The turn was cancelled via its CancellationToken | ❌ |
ContextBudgetExceeded { measured, budget } | Assembled prompt exceeded context_budget | ❌ |
Build(String) | AgentBuilder::build() misconfiguration | ❌ |
Serialization(_) | JSON (de)serialization failure | ❌ |
Io(_) | Underlying I/O failure | ❌ |
Build is the only variant that comes from build(); everything else is a
runtime error. An agent that constructs successfully at startup won’t surprise
you with a configuration error mid-request.
Classifying provider errors
Section titled “Classifying provider errors”KovaError::Provider carries a class: ProviderErrorClass (re-exported in the
prelude) so you can react to the kind of failure without parsing messages or
status codes. It’s derived from the HTTP status (Bedrock exception types and
Anthropic’s 529 are normalized first):
ProviderErrorClass | HTTP status | Meaning | Retryable? |
|---|---|---|---|
AuthInvalid | 401 | Credentials missing/malformed/revoked | ❌ |
AuthForbidden | 403 | Valid key, not permitted for this model/op | ❌ |
RateLimited { retry_after } | 429 | Throttled (retry_after from Retry-After) | ✅ |
Overloaded | 408 / 500 / 502 / 503 / 504 / 529 | Transient provider-side failure | ✅ |
InvalidRequest | 400 / 413 / 422 | Request rejected (incl. context-length) | ❌ |
NotFound | 404 | Unknown model or endpoint | ❌ |
Other | anything else | Including malformed responses | ❌ |
use kova_sdk::prelude::ProviderErrorClass;
match err.provider_class() { Some(ProviderErrorClass::AuthInvalid) => { /* prompt for a new key */ } Some(ProviderErrorClass::RateLimited { retry_after }) => { /* back off */ } Some(ProviderErrorClass::Overloaded) => { /* transient — retry */ } Some(ProviderErrorClass::InvalidRequest) => { /* fix the request */ } _ => {}}Provider errors are constructed through KovaError::provider_http(status, retry_after, message) (classifies from the status), provider_invalid(message)
(malformed responses → Other), and provider_auth(message) (pre-HTTP
credential failures → AuthInvalid).
Inspecting an error
Section titled “Inspecting an error”Two helpers avoid matching every variant when you just want the essentials:
// Is this worth retrying? (Kova already retries automatically; this is for your own logic.)if err.is_retryable() { /* transient — a retry might succeed */ }
// The HTTP status, if the error carries one.if let Some(code) = err.status_code() { eprintln!("HTTP {code}"); }
// The provider classification, if this is a provider error.if let Some(class) = err.provider_class() { eprintln!("{class:?}"); }is_retryable() is true for Connection, Timeout, and provider errors whose
class is RateLimited or Overloaded.
Handling errors
Section titled “Handling errors”match agent.run(&history).await { Ok(response) => println!("{}", response.text),
// The model API rejected the request (bad key, unknown model, rate limit, …) Err(KovaError::Provider { message, status_code, class }) => { eprintln!("provider error {status_code:?} [{class:?}]: {message}"); }
// The loop kept calling tools past its cap. Err(KovaError::MaxIterations(n)) => { eprintln!("gave up after {n} tool rounds — raise max_iterations or fix the prompt"); }
// The model asked for a tool you didn't register. Err(KovaError::ToolNotFound(name)) => { eprintln!("model wanted tool `{name}`, which isn't registered"); }
// The prompt was too big for the configured budget. Err(KovaError::ContextBudgetExceeded { measured, budget }) => { eprintln!("prompt ~{measured} tokens exceeds budget {budget} — trim history"); }
// The turn was cancelled — no messages were produced. Err(KovaError::Cancelled) => eprintln!("cancelled"),
// Transient network problems (already retried by default). Err(e) if e.is_retryable() => eprintln!("transient failure: {e}"),
Err(e) => eprintln!("error: {e}"),}Tool errors: Err vs. in-band
Section titled “Tool errors: Err vs. in-band”A crucial distinction (see Reliability & Errors):
KovaError::ToolExecutionis raised when your tool returnsErr(KovaError::…)— it aborts the turn.- Returning
Ok(ToolResult { is_error: true, content })is an in-band error: it goes back to the model, which can read it and recover, and does not produce aKovaError.
Prefer in-band errors for anything the model could work around. Reserve Err
(and thus ToolExecution) for genuine faults.
Retries happen automatically
Section titled “Retries happen automatically”You don’t have to retry retryable errors yourself — Kova does, with exponential
backoff (default 2 attempts), on every provider call and stream establishment.
By the time a retryable error reaches you, the retries have already been
exhausted. Configure via AgentBuilder::retry_config; see
Reliability & Errors.
Interop
Section titled “Interop”KovaError implements std::error::Error and Display, so it slots into ?,
anyhow, eyre, and other error stacks. Serialization and Io wrap the
underlying serde_json / std::io errors, which you can inspect if needed.