Skip to main content

claude_agent_sdk/
errors.rs

1//! Error types for the Claude Agent SDK
2//!
3//! This module provides a comprehensive error handling system with:
4//! - Error categories for classification
5//! - Error codes for machine-readable identification
6//! - Retryable detection for transient errors
7//! - HTTP status code mapping for API responses
8
9use std::path::PathBuf;
10use thiserror::Error;
11
12/// Error category for classifying errors by their source and nature.
13///
14/// Categories enable:
15/// - Structured logging with category filters
16/// - Metrics aggregation by error type
17/// - Retry logic based on error source
18/// - User-facing error messages by category
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ErrorCategory {
21    /// Network-related errors (connection, timeout, DNS)
22    Network,
23    /// CLI process errors (not found, crash, exit)
24    Process,
25    /// Data parsing/serialization errors (JSON, message format)
26    Parsing,
27    /// Configuration errors (invalid options, missing settings)
28    Configuration,
29    /// Input validation errors (invalid parameters, constraints)
30    Validation,
31    /// Permission/security errors (auth, access denied)
32    Permission,
33    /// Resource errors (not found, quota exceeded)
34    Resource,
35    /// Internal SDK errors (bugs, invariants violated)
36    Internal,
37    /// External service errors (API rate limits, service unavailable)
38    External,
39}
40
41impl ErrorCategory {
42    /// Returns true if errors in this category may be retried.
43    pub fn is_retryable(&self) -> bool {
44        matches!(
45            self,
46            ErrorCategory::Network | ErrorCategory::External | ErrorCategory::Process
47        )
48    }
49
50    /// Returns a human-readable description of the category.
51    pub fn description(&self) -> &'static str {
52        match self {
53            ErrorCategory::Network => "Network connectivity or communication error",
54            ErrorCategory::Process => "CLI process execution error",
55            ErrorCategory::Parsing => "Data parsing or serialization error",
56            ErrorCategory::Configuration => "Configuration or setup error",
57            ErrorCategory::Validation => "Input validation error",
58            ErrorCategory::Permission => "Permission or authentication error",
59            ErrorCategory::Resource => "Resource not found or unavailable",
60            ErrorCategory::Internal => "Internal SDK error",
61            ErrorCategory::External => "External service error",
62        }
63    }
64}
65
66impl std::fmt::Display for ErrorCategory {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            ErrorCategory::Network => write!(f, "network"),
70            ErrorCategory::Process => write!(f, "process"),
71            ErrorCategory::Parsing => write!(f, "parsing"),
72            ErrorCategory::Configuration => write!(f, "configuration"),
73            ErrorCategory::Validation => write!(f, "validation"),
74            ErrorCategory::Permission => write!(f, "permission"),
75            ErrorCategory::Resource => write!(f, "resource"),
76            ErrorCategory::Internal => write!(f, "internal"),
77            ErrorCategory::External => write!(f, "external"),
78        }
79    }
80}
81
82/// HTTP status code mapping for errors.
83///
84/// Provides a standard way to convert SDK errors to HTTP responses
85/// for API integrations.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HttpStatus {
88    /// 400 Bad Request - invalid input
89    BadRequest,
90    /// 401 Unauthorized - authentication required
91    Unauthorized,
92    /// 403 Forbidden - permission denied
93    Forbidden,
94    /// 404 Not Found - resource not found
95    NotFound,
96    /// 408 Request Timeout - operation timed out
97    RequestTimeout,
98    /// 409 Conflict - state conflict
99    Conflict,
100    /// 422 Unprocessable Entity - validation error
101    UnprocessableEntity,
102    /// 429 Too Many Requests - rate limited
103    TooManyRequests,
104    /// 500 Internal Server Error - SDK bug
105    InternalServerError,
106    /// 502 Bad Gateway - upstream error (CLI/API)
107    BadGateway,
108    /// 503 Service Unavailable - temporary error
109    ServiceUnavailable,
110    /// 504 Gateway Timeout - upstream timeout
111    GatewayTimeout,
112}
113
114impl HttpStatus {
115    /// Returns the numeric HTTP status code.
116    pub fn code(&self) -> u16 {
117        match self {
118            HttpStatus::BadRequest => 400,
119            HttpStatus::Unauthorized => 401,
120            HttpStatus::Forbidden => 403,
121            HttpStatus::NotFound => 404,
122            HttpStatus::RequestTimeout => 408,
123            HttpStatus::Conflict => 409,
124            HttpStatus::UnprocessableEntity => 422,
125            HttpStatus::TooManyRequests => 429,
126            HttpStatus::InternalServerError => 500,
127            HttpStatus::BadGateway => 502,
128            HttpStatus::ServiceUnavailable => 503,
129            HttpStatus::GatewayTimeout => 504,
130        }
131    }
132}
133
134impl From<HttpStatus> for u16 {
135    fn from(status: HttpStatus) -> u16 {
136        status.code()
137    }
138}
139
140/// Main error type for the Claude Agent SDK
141#[derive(Debug, Error)]
142pub enum ClaudeError {
143    /// CLI connection error
144    #[error("CLI connection error: {0}")]
145    Connection(#[from] ConnectionError),
146
147    /// Process error
148    #[error("Process error: {0}")]
149    Process(#[from] ProcessError),
150
151    /// JSON decode error
152    #[error("JSON decode error: {0}")]
153    JsonDecode(#[from] JsonDecodeError),
154
155    /// Message parse error
156    #[error("Message parse error: {0}")]
157    MessageParse(#[from] MessageParseError),
158
159    /// Transport error
160    #[error("Transport error: {0}")]
161    Transport(String),
162
163    /// Control protocol error
164    #[error("Control protocol error: {0}")]
165    ControlProtocol(String),
166
167    /// Invalid configuration
168    #[error("Invalid configuration: {0}")]
169    InvalidConfig(String),
170
171    /// CLI not found error
172    #[error("CLI not found: {0}")]
173    CliNotFound(#[from] CliNotFoundError),
174
175    /// Image validation error
176    #[error("Image validation error: {0}")]
177    ImageValidation(#[from] ImageValidationError),
178
179    /// IO error
180    #[error("IO error: {0}")]
181    Io(#[from] std::io::Error),
182
183    /// Other errors
184    #[error(transparent)]
185    Other(#[from] anyhow::Error),
186
187    /// Not found error
188    #[error("Not found: {0}")]
189    NotFound(String),
190
191    /// Invalid input error
192    #[error("Invalid input: {0}")]
193    InvalidInput(String),
194
195    /// Internal error
196    #[error("Internal error: {0}")]
197    InternalError(String),
198}
199
200/// Error when Claude Code CLI cannot be found
201#[derive(Debug, Error)]
202#[error("CLI not found: {message}")]
203pub struct CliNotFoundError {
204    /// Error message
205    pub message: String,
206    /// Path that was checked
207    pub cli_path: Option<PathBuf>,
208}
209
210impl CliNotFoundError {
211    /// Create a new CLI not found error
212    pub fn new(message: impl Into<String>, cli_path: Option<PathBuf>) -> Self {
213        Self {
214            message: message.into(),
215            cli_path,
216        }
217    }
218}
219
220/// Error when connecting to Claude Code CLI
221#[derive(Debug, Error)]
222#[error("Connection error: {message}")]
223pub struct ConnectionError {
224    /// Error message
225    pub message: String,
226}
227
228impl ConnectionError {
229    /// Create a new connection error
230    pub fn new(message: impl Into<String>) -> Self {
231        Self {
232            message: message.into(),
233        }
234    }
235}
236
237/// Error when the CLI process fails
238#[derive(Debug, Error)]
239#[error("Process error (exit code {exit_code:?}): {message}")]
240pub struct ProcessError {
241    /// Error message
242    pub message: String,
243    /// Process exit code
244    pub exit_code: Option<i32>,
245    /// stderr output
246    pub stderr: Option<String>,
247}
248
249impl ProcessError {
250    /// Create a new process error
251    pub fn new(message: impl Into<String>, exit_code: Option<i32>, stderr: Option<String>) -> Self {
252        Self {
253            message: message.into(),
254            exit_code,
255            stderr,
256        }
257    }
258}
259
260/// Error when JSON decoding fails
261#[derive(Debug, Error)]
262#[error("JSON decode error: {message}")]
263pub struct JsonDecodeError {
264    /// Error message
265    pub message: String,
266    /// The line that failed to decode
267    pub line: String,
268}
269
270impl JsonDecodeError {
271    /// Create a new JSON decode error
272    pub fn new(message: impl Into<String>, line: impl Into<String>) -> Self {
273        Self {
274            message: message.into(),
275            line: line.into(),
276        }
277    }
278}
279
280/// Error when message parsing fails
281#[derive(Debug, Error)]
282#[error("Message parse error: {message}")]
283pub struct MessageParseError {
284    /// Error message
285    pub message: String,
286    /// The data that failed to parse
287    pub data: Option<serde_json::Value>,
288}
289
290impl MessageParseError {
291    /// Create a new message parse error
292    pub fn new(message: impl Into<String>, data: Option<serde_json::Value>) -> Self {
293        Self {
294            message: message.into(),
295            data,
296        }
297    }
298}
299
300/// Image validation error
301#[derive(Debug, Error)]
302#[error("Image validation error: {message}")]
303pub struct ImageValidationError {
304    /// Error message
305    pub message: String,
306}
307
308impl ImageValidationError {
309    /// Create a new image validation error
310    pub fn new(message: impl Into<String>) -> Self {
311        Self {
312            message: message.into(),
313        }
314    }
315}
316
317/// Result type for the Claude Agent SDK
318pub type Result<T> = std::result::Result<T, ClaudeError>;
319
320impl ClaudeError {
321    /// Returns the error category for this error.
322    ///
323    /// Categories group related errors together for:
324    /// - Structured logging and filtering
325    /// - Metrics aggregation
326    /// - Retry logic decisions
327    pub fn category(&self) -> ErrorCategory {
328        match self {
329            ClaudeError::Connection(_) => ErrorCategory::Network,
330            ClaudeError::Process(_) => ErrorCategory::Process,
331            ClaudeError::JsonDecode(_) => ErrorCategory::Parsing,
332            ClaudeError::MessageParse(_) => ErrorCategory::Parsing,
333            ClaudeError::Transport(_) => ErrorCategory::Network,
334            ClaudeError::ControlProtocol(_) => ErrorCategory::Internal,
335            ClaudeError::InvalidConfig(_) => ErrorCategory::Configuration,
336            ClaudeError::CliNotFound(_) => ErrorCategory::Configuration,
337            ClaudeError::ImageValidation(_) => ErrorCategory::Validation,
338            ClaudeError::Io(_) => ErrorCategory::Internal,
339            ClaudeError::Other(_) => ErrorCategory::Internal,
340            ClaudeError::NotFound(_) => ErrorCategory::Resource,
341            ClaudeError::InvalidInput(_) => ErrorCategory::Validation,
342            ClaudeError::InternalError(_) => ErrorCategory::Internal,
343        }
344    }
345
346    /// Returns a machine-readable error code for this error.
347    ///
348    /// Error codes are stable identifiers that can be used for:
349    /// - Programmatic error handling
350    /// - API response codes
351    /// - Documentation references
352    ///
353    /// Format: `E{category_prefix}{number}` (e.g., `ENET001`, `EPROC001`)
354    pub fn error_code(&self) -> &'static str {
355        match self {
356            ClaudeError::Connection(_) => "ENET001",
357            ClaudeError::Process(_) => "EPROC001",
358            ClaudeError::JsonDecode(_) => "EPARSE001",
359            ClaudeError::MessageParse(_) => "EPARSE002",
360            ClaudeError::Transport(_) => "ENET002",
361            ClaudeError::ControlProtocol(_) => "EINT001",
362            ClaudeError::InvalidConfig(_) => "ECFG001",
363            ClaudeError::CliNotFound(_) => "ECFG002",
364            ClaudeError::ImageValidation(_) => "EVAL001",
365            ClaudeError::Io(_) => "EINT002",
366            ClaudeError::Other(_) => "EINT003",
367            ClaudeError::NotFound(_) => "ERES001",
368            ClaudeError::InvalidInput(_) => "EVAL002",
369            ClaudeError::InternalError(_) => "EINT004",
370        }
371    }
372
373    /// Returns true if this error may be retried.
374    ///
375    /// Retryable errors are typically transient failures that may succeed
376    /// on a subsequent attempt:
377    /// - Network errors (connection refused, timeout)
378    /// - Process errors (CLI crashed, can restart)
379    /// - External service errors (rate limits, temporary unavailable)
380    pub fn is_retryable(&self) -> bool {
381        self.category().is_retryable()
382    }
383
384    /// Returns the recommended HTTP status code for this error.
385    ///
386    /// Useful for converting SDK errors to HTTP API responses.
387    pub fn http_status(&self) -> HttpStatus {
388        match self {
389            ClaudeError::Connection(_) => HttpStatus::ServiceUnavailable,
390            ClaudeError::Process(_) => HttpStatus::BadGateway,
391            ClaudeError::JsonDecode(_) => HttpStatus::UnprocessableEntity,
392            ClaudeError::MessageParse(_) => HttpStatus::UnprocessableEntity,
393            ClaudeError::Transport(_) => HttpStatus::ServiceUnavailable,
394            ClaudeError::ControlProtocol(_) => HttpStatus::InternalServerError,
395            ClaudeError::InvalidConfig(_) => HttpStatus::InternalServerError,
396            ClaudeError::CliNotFound(_) => HttpStatus::InternalServerError,
397            ClaudeError::ImageValidation(_) => HttpStatus::BadRequest,
398            ClaudeError::Io(_) => HttpStatus::InternalServerError,
399            ClaudeError::Other(_) => HttpStatus::InternalServerError,
400            ClaudeError::NotFound(_) => HttpStatus::NotFound,
401            ClaudeError::InvalidInput(_) => HttpStatus::BadRequest,
402            ClaudeError::InternalError(_) => HttpStatus::InternalServerError,
403        }
404    }
405
406    /// Returns a detailed error context for logging and debugging.
407    ///
408    /// Includes:
409    /// - Error code
410    /// - Category
411    /// - Whether it's retryable
412    /// - Human-readable message
413    pub fn to_error_context(&self) -> ErrorContext {
414        ErrorContext {
415            code: self.error_code().to_string(),
416            category: self.category(),
417            message: self.to_string(),
418            retryable: self.is_retryable(),
419            http_status: self.http_status().code(),
420        }
421    }
422}
423
424/// Detailed error context for logging and debugging.
425#[derive(Debug, Clone)]
426pub struct ErrorContext {
427    /// Machine-readable error code (e.g., "ENET001")
428    pub code: String,
429    /// Error category
430    pub category: ErrorCategory,
431    /// Human-readable error message
432    pub message: String,
433    /// Whether the error may be retried
434    pub retryable: bool,
435    /// Recommended HTTP status code
436    pub http_status: u16,
437}
438
439impl std::fmt::Display for ErrorContext {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        write!(
442            f,
443            "[{}] [{}] {} (retryable: {}, http: {})",
444            self.code, self.category, self.message, self.retryable, self.http_status
445        )
446    }
447}
448
449impl std::fmt::Display for HttpStatus {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        match self {
452            HttpStatus::BadRequest => write!(f, "400 Bad Request"),
453            HttpStatus::Unauthorized => write!(f, "401 Unauthorized"),
454            HttpStatus::Forbidden => write!(f, "403 Forbidden"),
455            HttpStatus::NotFound => write!(f, "404 Not Found"),
456            HttpStatus::RequestTimeout => write!(f, "408 Request Timeout"),
457            HttpStatus::Conflict => write!(f, "409 Conflict"),
458            HttpStatus::UnprocessableEntity => write!(f, "422 Unprocessable Entity"),
459            HttpStatus::TooManyRequests => write!(f, "429 Too Many Requests"),
460            HttpStatus::InternalServerError => write!(f, "500 Internal Server Error"),
461            HttpStatus::BadGateway => write!(f, "502 Bad Gateway"),
462            HttpStatus::ServiceUnavailable => write!(f, "503 Service Unavailable"),
463            HttpStatus::GatewayTimeout => write!(f, "504 Gateway Timeout"),
464        }
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn test_error_categories() {
474        let error = ClaudeError::Connection(ConnectionError::new("test"));
475        assert_eq!(error.category(), ErrorCategory::Network);
476        assert!(error.is_retryable());
477        assert_eq!(error.error_code(), "ENET001");
478
479        let error = ClaudeError::InvalidConfig("test".to_string());
480        assert_eq!(error.category(), ErrorCategory::Configuration);
481        assert!(!error.is_retryable());
482        assert_eq!(error.error_code(), "ECFG001");
483    }
484
485    #[test]
486    fn test_http_status_mapping() {
487        let error = ClaudeError::NotFound("test".to_string());
488        assert_eq!(error.http_status(), HttpStatus::NotFound);
489        assert_eq!(error.http_status().code(), 404);
490
491        let error = ClaudeError::InvalidInput("test".to_string());
492        assert_eq!(error.http_status(), HttpStatus::BadRequest);
493        assert_eq!(error.http_status().code(), 400);
494    }
495
496    #[test]
497    fn test_error_context() {
498        let error = ClaudeError::Connection(ConnectionError::new("connection failed"));
499        let ctx = error.to_error_context();
500        assert_eq!(ctx.code, "ENET001");
501        assert_eq!(ctx.category, ErrorCategory::Network);
502        assert!(ctx.retryable);
503        assert_eq!(ctx.http_status, 503);
504    }
505
506    #[test]
507    fn test_category_display() {
508        assert_eq!(ErrorCategory::Network.to_string(), "network");
509        assert_eq!(ErrorCategory::Process.to_string(), "process");
510        assert_eq!(ErrorCategory::Parsing.to_string(), "parsing");
511    }
512
513    #[test]
514    fn test_category_description() {
515        assert!(!ErrorCategory::Network.description().is_empty());
516        assert!(ErrorCategory::Network.is_retryable());
517        assert!(!ErrorCategory::Configuration.is_retryable());
518    }
519}