Skip to main content

lc_core/runnables/
error.rs

1// lc-core/src/runnables/error.rs
2//! Unified error type for LCEL (LangChain Expression Language) pipelines.
3//!
4//! `LcelError` bridges all sub-crate error types into a single enum,
5//! enabling `RunnableSequence` and other LCEL combinators to work with
6//! a uniform error type regardless of which components are in the pipeline.
7//!
8//! # Design Decision
9//!
10//! `LcelError` stores error descriptions as `String` rather than wrapping
11//! concrete sub-error types (e.g. `Chain(ChainError)`). This avoids:
12//! 1. Circular dependencies between `lc-core` and downstream crates
13//! 2. Type-erased pipeline steps where the concrete error type is lost anyway
14//! 3. Bloating the enum with every provider-specific error variant
15//!
16//! The `Display` representation preserves enough information for debugging.
17
18use std::fmt;
19
20/// Unified error type for LCEL pipelines.
21///
22/// All `Runnable` components that participate in LCEL composition
23/// must have an `Error` type that implements `Into<LcelError>`.
24#[derive(Debug, Clone)]
25pub enum LcelError {
26    /// Error from an LLM provider (OpenAI, Anthropic, Gemini, Ollama, etc.).
27    Provider(String),
28
29    /// Error from a chain execution.
30    Chain(String),
31
32    /// Error from an agent execution.
33    Agent(String),
34
35    /// Error from a graph execution.
36    Graph(String),
37
38    /// Error from a tool execution.
39    Tool(String),
40
41    /// Error from an output parser.
42    OutputParser(String),
43
44    /// Error during streaming.
45    Stream(String),
46
47    /// Error in pipeline composition or execution.
48    Pipeline(String),
49
50    /// Type-erasure downcast failure.
51    TypeMismatch(String),
52
53    /// Catch-all for errors that don't fit other variants.
54    Other(String),
55}
56
57impl fmt::Display for LcelError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            LcelError::Provider(msg) => write!(f, "Provider error: {msg}"),
61            LcelError::Chain(msg) => write!(f, "Chain error: {msg}"),
62            LcelError::Agent(msg) => write!(f, "Agent error: {msg}"),
63            LcelError::Graph(msg) => write!(f, "Graph error: {msg}"),
64            LcelError::Tool(msg) => write!(f, "Tool error: {msg}"),
65            LcelError::OutputParser(msg) => write!(f, "Output parser error: {msg}"),
66            LcelError::Stream(msg) => write!(f, "Stream error: {msg}"),
67            LcelError::Pipeline(msg) => write!(f, "Pipeline error: {msg}"),
68            LcelError::TypeMismatch(msg) => write!(f, "Type mismatch: {msg}"),
69            LcelError::Other(msg) => write!(f, "{msg}"),
70        }
71    }
72}
73
74impl std::error::Error for LcelError {}
75
76// Allow `Infallible` to convert into `LcelError` (never actually happens).
77impl From<std::convert::Infallible> for LcelError {
78    fn from(_: std::convert::Infallible) -> Self {
79        unreachable!()
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn display_formats_correctly() {
89        assert_eq!(
90            LcelError::Provider("openai timeout".to_string()).to_string(),
91            "Provider error: openai timeout"
92        );
93        assert_eq!(
94            LcelError::Chain("missing input".to_string()).to_string(),
95            "Chain error: missing input"
96        );
97        assert_eq!(
98            LcelError::TypeMismatch("expected String got i32".to_string()).to_string(),
99            "Type mismatch: expected String got i32"
100        );
101    }
102
103    #[test]
104    fn is_send_sync() {
105        fn assert_send_sync<T: Send + Sync + 'static>() {}
106        assert_send_sync::<LcelError>();
107    }
108}