Error handling
Every fallible Rig call returns a typed error, so you can react to why something failed instead of parsing strings. Two types cover almost everything:
PromptError: an agent run failed (agent.prompt(..).await, or the last item of.stream()).ProviderError: a call to a provider failed: completions, embeddings, transcription, image generation, model listing. One type for every operation; the variant says what went wrong.
Typed prompts and extractors wrap PromptError in a StructuredOutputError (see
Structured Output); vector stores return VectorStoreError.
Errors from an agent run
Section titled “Errors from an agent run”agent.prompt(..).await returns Result<PromptResponse, PromptError>. PromptError is
#[non_exhaustive], so a match needs a catch-all arm:
match agent.prompt("What is 2 + 2?").max_turns(3).await { Ok(response) => println!("{}", response.output()), Err(PromptError::Provider(error)) => { eprintln!("the model call failed ({:?}): {error}", error.kind()); } Err(PromptError::MaxTurns { max_turns, .. }) => { eprintln!("used all {max_turns} model calls without a final answer"); } Err(PromptError::UnknownToolCall { tool_name, .. }) => { eprintln!("the model called `{tool_name}`, which this agent doesn't have"); } Err(PromptError::Cancelled { reason, .. }) => eprintln!("cancelled: {reason}"), Err(error) => eprintln!("run failed: {error}"),}| Variant | Meaning |
|---|---|
Provider(ProviderError) | A model call failed: network, rate limit, bad request, unparseable reply, or a turn the provider ended with an error. |
Report(ErrorReport) | A failure delivered as a serialized report, for example from a hook, a handler on the effect bus, or a relayed stream. Same fields as below. |
Memory(MemoryError) | Conversation memory failed to load or save history. |
MaxTurns { max_turns, chat_history, prompt } | The run used its whole model-call budget. Raise it with .max_turns(n) or .default_max_turns(n). |
Cancelled { reason, chat_history } | A hook stopped the run. |
UnknownToolCall { tool_name, available_tools, allowed_tools, .. } | The model called a tool the agent doesn’t offer this turn, and no hook recovered it. |
The variants that end mid-run carry chat_history, the conversation up to the failure, so you can
log it or resume from it.
A tool that returns an error does not produce a PromptError: the error goes back to the model
as the tool’s result and the run continues. See
When a tool fails. A hook can still end the run on a
specific tool failure; that shows up as Cancelled.
Errors from a provider
Section titled “Errors from a provider”ProviderError is what a direct
model call returns (model.call(..), model.stream(..), embedding_model.embed_text(..)), and
what PromptError::Provider wraps. Its main variants:
| Variant | Meaning |
|---|---|
Http(..) | Transport failure with no reply: connection reset, timeout, unreadable response. |
ProviderResponse(ProviderResponseError) | The provider replied with an error: a non-2xx status, or an error body with a 2xx status. The reply is preserved. |
InvalidAuthentication(ProviderResponseError) | The provider rejected the credentials (401 or 403). |
Json(..) | A request or reply couldn’t be (de)serialized. |
Request(..) | The request couldn’t be built. |
UnsupportedOption(..) | The request set an option (reasoning, caching, …) the model or API can’t honour. This is the default OnUnsupported::Error policy; OnUnsupported::Ignore skips the option instead. |
Response(String) | The reply decoded but doesn’t answer the request, or the provider ended the turn with an error. |
Truncated | A reply stopped before the provider finished it, for example a stream cut off mid-response. |
ProviderError is also #[non_exhaustive]. Rather than matching every variant, use the helpers:
error.is_retryable(): whether the same request may succeed if sent again. Transport failures, timeouts, HTTP 408/429/5xx and truncated replies are retryable; bad requests, rejected credentials and malformed replies are not.error.kind(): anErrorKind, the same classification for every operation.error.provider_response(): the preserved reply as aProviderResponseError, with itsstatus, rawbody,headersandprovider_request_id. There are shortcuts for each:provider_response_status(),provider_response_body(),provider_response_json(),provider_response_headers()andprovider_request_id().
use rig::ProviderError;
fn log_failure(error: &ProviderError) { eprintln!("{:?} (retryable: {}): {error}", error.kind(), error.is_retryable()); if let Some(status) = error.provider_response_status() { eprintln!("provider returned HTTP {status}"); } if let Some(request_id) = error.provider_request_id() { eprintln!("quote this request id to the provider: {request_id}"); } match error.provider_response_json() { Ok(Some(body)) => eprintln!("error payload: {body}"), Ok(None) => {} Err(_) => eprintln!("non-JSON body: {:?}", error.provider_response_body()), }}PromptError forwards the same provider_response_* accessors, so you can read the provider’s reply
without unwrapping the variant first.
Error reports
Section titled “Error reports”ProviderError holds live values (sources, header maps) and is not Serialize. To store or send an
error, convert it with error.report() into an
ErrorReport: a plain struct with
kind, retryable, message, code, http_status, request_id, the source_chain as text, and
the preserved provider_response. PromptError::Report carries one of these, so the same checks
work on both:
use rig::completion::PromptError;
fn is_retryable(error: &PromptError) -> bool { match error { PromptError::Provider(error) => error.is_retryable(), PromptError::Report(report) => report.is_retryable(), _ => false, }}Unknown finish reasons fail the run
Section titled “Unknown finish reasons fail the run”Every model reply ends with a finish reason. Rig accepts the normal ones (stop, tool calls, token
limit). A reply that ends with a reason outside that set fails the turn with
PromptError::Provider(ProviderError::Response(..)), and its tool calls don’t run, because such
reasons often mean the reply is broken (Gemini’s MALFORMED_FUNCTION_CALL, Bedrock’s
malformed_tool_use). A content-filter stop also fails the turn.
If a provider uses a finish reason Rig doesn’t know for ordinary replies, accept unknown reasons as a normal stop, for every run or for one:
let agent = AgentBuilder::new(model) .accept_unknown_finish_reasons(true) .build();
let reply = agent .prompt("Hello") .accept_unknown_finish_reasons(true) .await?;A turn that ran out of output tokens before producing any answer also fails, with a message saying
so; raise max_tokens if you see it.
Retrying transient failures
Section titled “Retrying transient failures”Rig doesn’t retry failed model calls for you. Wrap the call in a backoff loop and retry only errors that are worth retrying:
use rig::agent::{Agent, PromptResponse};use rig::completion::PromptError;use std::time::Duration;
/// Prompt an agent, retrying transient failures with exponential backoff.async fn prompt_with_retry( agent: &Agent, input: &str, max_attempts: u32,) -> Result<PromptResponse, PromptError> { let mut attempt = 0; loop { attempt += 1; let error = match agent.prompt(input).await { Ok(response) => return Ok(response), Err(error) => error, }; let retryable = match &error { PromptError::Provider(e) => e.is_retryable(), PromptError::Report(report) => report.is_retryable(), _ => false, }; if !retryable || attempt >= max_attempts { return Err(error); } // Prefer the provider's Retry-After header, else back off exponentially. let backoff = error .provider_response_headers() .and_then(|headers| headers.get("retry-after")?.to_str().ok()?.parse().ok()) .map(Duration::from_secs) .unwrap_or(Duration::from_millis(200 * 2u64.pow(attempt - 1))); eprintln!("attempt {attempt} failed ({error}); retrying in {backoff:?}"); tokio::time::sleep(backoff).await; }}attempt 1 failed (provider returned 429 Too Many Requests: ...); retrying in 20sA retried prompt starts the run over, including any tool calls it already made. For heavy workloads,
also cap concurrency in front of your agents (for example a tokio::sync::Semaphore) so you throttle
before the provider does.
Retrying a model turn because of its content (the answer is missing a required marker, or failed
validation) is a different job: a hook can reject a finished turn and ask for another attempt within
the run’s turn budget. See Hooks and the
agent_with_retry_hook
example.
