Error Handling Guide
FerricLink provides a comprehensive error handling system that helps you build robust AI applications. This guide covers best practices and patterns for handling errors effectively.
Error Types
FerricLink uses structured error types that provide detailed information about what went wrong and how to fix it.
Basic Error Handling
use ferriclink_core::{FerricLinkError, Result};
fn process_data(data: &str) -> Result<String> {
if data.is_empty() {
return Err(FerricLinkError::validation("Data cannot be empty"));
}
// Process the data
Ok(data.to_uppercase())
}
Error Code Checking
use ferriclink_core::{FerricLinkError, ErrorCode};
fn handle_error(error: FerricLinkError) {
match error.error_code() {
Some(ErrorCode::ModelRateLimit) => {
println!("Rate limited - retry later");
}
Some(ErrorCode::OutputParsingFailure) => {
println!("Parsing failed - check output format");
}
Some(ErrorCode::ModelAuthentication) => {
println!("Authentication failed - check API key");
}
_ => {
println!("Other error: {}", error);
}
}
}
LLM Feedback Patterns
FerricLink supports sending feedback to language models when parsing fails, allowing for automatic retries.
Basic LLM Feedback
use ferriclink_core::{FerricLinkError, OutputParserException};
async fn parse_with_retry(output: &str) -> Result<serde_json::Value, FerricLinkError> {
match serde_json::from_str::<serde_json::Value>(output) {
Ok(parsed) => Ok(parsed),
Err(e) => {
// Create parser exception with LLM feedback
let parser_err = OutputParserException::with_llm_context(
format!("Invalid JSON: {}", e),
Some("Please provide valid JSON output".to_string()),
Some(output.to_string()),
true, // Send back to LLM
);
Err(parser_err.into())
}
}
}
Retry with Feedback
async fn retry_with_feedback(error: FerricLinkError) -> Result<String, FerricLinkError> {
if error.should_send_to_llm() {
if let Some((observation, llm_output)) = error.llm_context() {
let retry_prompt = format!(
"Previous output was invalid: {}\nObservation: {}\nPlease try again.",
llm_output.unwrap_or(""),
observation.unwrap_or("")
);
// Send retry request to LLM
return call_llm_with_prompt(&retry_prompt).await;
}
}
Err(error)
}
Error Recovery Patterns
Exponential Backoff
use ferriclink_core::FerricLinkError;
use tokio::time::{sleep, Duration};
async fn retry_with_backoff<F, T>(
mut operation: F,
max_retries: usize,
) -> Result<T, FerricLinkError>
where
F: FnMut() -> Result<T, FerricLinkError>,
{
let mut delay = Duration::from_secs(1);
for attempt in 0..max_retries {
match operation() {
Ok(result) => return Ok(result),
Err(e) => {
if attempt < max_retries - 1 {
println!("Attempt {} failed: {}, retrying in {:?}",
attempt + 1, e, delay);
sleep(delay).await;
delay *= 2; // Exponential backoff
} else {
return Err(e);
}
}
}
}
Err(FerricLinkError::runtime("Max retries exceeded"))
}
Circuit Breaker Pattern
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
struct CircuitBreaker {
is_open: Arc<AtomicBool>,
last_failure: Arc<Mutex<Option<Instant>>>,
timeout: Duration,
}
impl CircuitBreaker {
fn new(timeout: Duration) -> Self {
Self {
is_open: Arc::new(AtomicBool::new(false)),
last_failure: Arc::new(Mutex::new(None)),
timeout,
}
}
async fn call<F, T>(&self, operation: F) -> Result<T, FerricLinkError>
where
F: FnOnce() -> Result<T, FerricLinkError>,
{
if self.is_open.load(Ordering::Relaxed) {
if let Some(last_failure) = *self.last_failure.lock().await {
if last_failure.elapsed() < self.timeout {
return Err(FerricLinkError::runtime("Circuit breaker is open"));
} else {
// Reset circuit breaker
self.is_open.store(false, Ordering::Relaxed);
}
}
}
match operation() {
Ok(result) => {
// Reset on success
self.is_open.store(false, Ordering::Relaxed);
Ok(result)
}
Err(e) => {
// Open circuit breaker on failure
self.is_open.store(true, Ordering::Relaxed);
*self.last_failure.lock().await = Some(Instant::now());
Err(e)
}
}
}
}
Logging and Monitoring
Structured Logging
use ferriclink_core::FerricLinkError;
use tracing::{error, warn, info};
fn log_error(error: &FerricLinkError) {
match error.error_code() {
Some(ErrorCode::ModelRateLimit) => {
warn!("Rate limit exceeded: {}", error);
}
Some(ErrorCode::ModelAuthentication) => {
error!("Authentication failed: {}", error);
}
Some(ErrorCode::OutputParsingFailure) => {
info!("Parsing failed, will retry: {}", error);
}
_ => {
error!("Unexpected error: {}", error);
}
}
}
Error Metrics
use std::sync::atomic::{AtomicU64, Ordering};
struct ErrorMetrics {
total_errors: AtomicU64,
rate_limit_errors: AtomicU64,
parsing_errors: AtomicU64,
}
impl ErrorMetrics {
fn record_error(&self, error: &FerricLinkError) {
self.total_errors.fetch_add(1, Ordering::Relaxed);
match error.error_code() {
Some(ErrorCode::ModelRateLimit) => {
self.rate_limit_errors.fetch_add(1, Ordering::Relaxed);
}
Some(ErrorCode::OutputParsingFailure) => {
self.parsing_errors.fetch_add(1, Ordering::Relaxed);
}
_ => {}
}
}
}
Best Practices
- Always handle errors explicitly - Don't ignore
Result
types - Use specific error types - Choose the most appropriate error type
- Provide context - Include relevant information in error messages
- Implement retries - For transient errors like rate limits
- Log errors appropriately - Use appropriate log levels
- Monitor error rates - Track error patterns and trends
- Test error scenarios - Include error cases in your tests