Skip to main content

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