cc_sdk/
errors.rs

1//! Error types for the Claude Code SDK
2//!
3//! This module defines all error types that can occur when using the SDK.
4//! The errors are designed to be informative and actionable, helping users
5//! understand what went wrong and how to fix it.
6
7use thiserror::Error;
8
9/// Main error type for the Claude Code SDK
10#[derive(Error, Debug)]
11pub enum SdkError {
12    /// Claude CLI executable was not found
13    #[error(
14        "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n\nSearched in:\n{searched_paths}"
15    )]
16    CliNotFound {
17        /// Paths that were searched for the CLI
18        searched_paths: String,
19    },
20
21    /// Failed to connect to Claude CLI
22    #[error("Failed to connect to Claude CLI: {0}")]
23    ConnectionError(String),
24
25    /// Process-related errors
26    #[error("Process error: {0}")]
27    ProcessError(#[from] std::io::Error),
28
29    /// Failed to parse a message
30    #[error("Failed to parse message: {error}\nRaw message: {raw}")]
31    MessageParseError {
32        /// Parse error description
33        error: String,
34        /// Raw message that failed to parse
35        raw: String,
36    },
37
38    /// JSON serialization/deserialization errors
39    #[error("JSON error: {0}")]
40    JsonError(#[from] serde_json::Error),
41
42    /// CLI JSON decode error
43    #[error("Failed to decode JSON from CLI output: {line}")]
44    CliJsonDecodeError {
45        /// Line that failed to decode
46        line: String,
47        /// Original error
48        #[source]
49        original_error: serde_json::Error,
50    },
51
52    /// Transport layer errors
53    #[error("Transport error: {0}")]
54    TransportError(String),
55
56    /// Timeout waiting for response
57    #[error("Timeout waiting for response after {seconds} seconds")]
58    Timeout {
59        /// Number of seconds waited before timeout
60        seconds: u64,
61    },
62
63    /// Session not found
64    #[error("Session not found: {0}")]
65    SessionNotFound(String),
66
67    /// Invalid configuration
68    #[error("Invalid configuration: {0}")]
69    ConfigError(String),
70
71    /// Control request failed
72    #[error("Control request failed: {0}")]
73    ControlRequestError(String),
74
75    /// Unexpected response type
76    #[error("Unexpected response type: expected {expected}, got {actual}")]
77    UnexpectedResponse {
78        /// Expected response type
79        expected: String,
80        /// Actual response type received
81        actual: String,
82    },
83
84    /// CLI returned an error
85    #[error("Claude CLI error: {message}")]
86    CliError {
87        /// Error message from CLI
88        message: String,
89        /// Error code if available
90        code: Option<String>,
91    },
92
93    /// Channel send error
94    #[error("Failed to send message through channel")]
95    ChannelSendError,
96
97    /// Channel receive error
98    #[error("Channel closed unexpectedly")]
99    ChannelClosed,
100
101    /// Invalid state transition
102    #[error("Invalid state: {message}")]
103    InvalidState {
104        /// Description of the invalid state
105        message: String,
106    },
107
108    /// Process exited unexpectedly
109    #[error("Claude process exited unexpectedly with code {code:?}")]
110    ProcessExited {
111        /// Exit code if available
112        code: Option<i32>,
113    },
114
115    /// Stream ended unexpectedly
116    #[error("Stream ended unexpectedly")]
117    UnexpectedStreamEnd,
118
119    /// Feature not supported
120    #[error("Feature not supported: {feature}")]
121    NotSupported {
122        /// Description of unsupported feature
123        feature: String,
124    },
125}
126
127/// Result type alias for SDK operations
128pub type Result<T> = std::result::Result<T, SdkError>;
129
130impl SdkError {
131    /// Create a new MessageParseError
132    pub fn parse_error(error: impl Into<String>, raw: impl Into<String>) -> Self {
133        Self::MessageParseError {
134            error: error.into(),
135            raw: raw.into(),
136        }
137    }
138
139    /// Create a new Timeout error
140    pub fn timeout(seconds: u64) -> Self {
141        Self::Timeout { seconds }
142    }
143
144    /// Create a new UnexpectedResponse error
145    pub fn unexpected_response(expected: impl Into<String>, actual: impl Into<String>) -> Self {
146        Self::UnexpectedResponse {
147            expected: expected.into(),
148            actual: actual.into(),
149        }
150    }
151
152    /// Create a new CliError
153    pub fn cli_error(message: impl Into<String>, code: Option<String>) -> Self {
154        Self::CliError {
155            message: message.into(),
156            code,
157        }
158    }
159
160    /// Create a new InvalidState error
161    pub fn invalid_state(message: impl Into<String>) -> Self {
162        Self::InvalidState {
163            message: message.into(),
164        }
165    }
166
167    /// Check if the error is recoverable
168    pub fn is_recoverable(&self) -> bool {
169        matches!(
170            self,
171            Self::Timeout { .. }
172                | Self::ChannelClosed
173                | Self::UnexpectedStreamEnd
174                | Self::ProcessExited { .. }
175        )
176    }
177
178    /// Check if the error is a configuration issue
179    pub fn is_config_error(&self) -> bool {
180        matches!(
181            self,
182            Self::CliNotFound { .. } | Self::ConfigError(_) | Self::NotSupported { .. }
183        )
184    }
185}
186
187// Implement From for common channel errors
188impl<T> From<tokio::sync::mpsc::error::SendError<T>> for SdkError {
189    fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self {
190        Self::ChannelSendError
191    }
192}
193
194impl From<tokio::sync::broadcast::error::RecvError> for SdkError {
195    fn from(_: tokio::sync::broadcast::error::RecvError) -> Self {
196        Self::ChannelClosed
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_error_display() {
206        let err = SdkError::CliNotFound {
207            searched_paths: "/usr/local/bin\n/usr/bin".to_string(),
208        };
209        let msg = err.to_string();
210        assert!(msg.contains("npm install -g @anthropic-ai/claude-code"));
211        assert!(msg.contains("/usr/local/bin"));
212    }
213
214    #[test]
215    fn test_is_recoverable() {
216        assert!(SdkError::timeout(30).is_recoverable());
217        assert!(SdkError::ChannelClosed.is_recoverable());
218        assert!(!SdkError::ConfigError("test".into()).is_recoverable());
219    }
220
221    #[test]
222    fn test_is_config_error() {
223        assert!(SdkError::ConfigError("test".into()).is_config_error());
224        assert!(
225            SdkError::CliNotFound {
226                searched_paths: "test".into()
227            }
228            .is_config_error()
229        );
230        assert!(!SdkError::timeout(30).is_config_error());
231    }
232
233    #[test]
234    fn test_cli_json_decode_error() {
235        let line = r#"{"invalid": json"#.to_string();
236        let original_err = serde_json::from_str::<serde_json::Value>(&line).unwrap_err();
237        
238        let error = SdkError::CliJsonDecodeError {
239            line: line.clone(),
240            original_error: original_err,
241        };
242        
243        let error_str = error.to_string();
244        assert!(error_str.contains("Failed to decode JSON from CLI output"));
245        assert!(error_str.contains(&line));
246    }
247}