Skip to main content

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&lt;AtomicBool&gt;,
last_failure: Arc&lt;Mutex&lt;Option&lt;Instant&gt;&gt;&gt;,
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

  1. Always handle errors explicitly - Don't ignore Result types
  2. Use specific error types - Choose the most appropriate error type
  3. Provide context - Include relevant information in error messages
  4. Implement retries - For transient errors like rate limits
  5. Log errors appropriately - Use appropriate log levels
  6. Monitor error rates - Track error patterns and trends
  7. Test error scenarios - Include error cases in your tests