Skip to main content

claude_code/
error.rs

1use std::io;
2
3/// Error types for claude-code.
4#[derive(Debug, thiserror::Error)]
5#[non_exhaustive]
6pub enum ClaudeError {
7    /// `claude` command not found in PATH.
8    #[error(
9        "claude CLI not found in PATH. Install it from https://docs.anthropic.com/en/docs/claude-code"
10    )]
11    CliNotFound,
12
13    /// CLI exited with a non-zero status code.
14    #[error("claude exited with code {code}: {stderr}")]
15    NonZeroExit {
16        /// Exit code.
17        code: i32,
18        /// Captured stderr content.
19        stderr: String,
20    },
21
22    /// Failed to deserialize JSON / stream-json response.
23    #[error("failed to parse response")]
24    ParseError(#[from] serde_json::Error),
25
26    /// Request timed out.
27    #[error("request timed out")]
28    Timeout,
29
30    /// I/O error from process spawn, stdout/stderr reads, etc.
31    #[error(transparent)]
32    Io(#[from] io::Error),
33
34    /// CLI succeeded but the `result` field could not be deserialized
35    /// into the target type.
36    #[error("failed to deserialize structured output: {source}")]
37    StructuredOutputError {
38        /// Raw result string from CLI.
39        raw_result: String,
40        /// Deserialization error.
41        source: serde_json::Error,
42    },
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn cli_not_found_message() {
51        let err = ClaudeError::CliNotFound;
52        assert_eq!(
53            err.to_string(),
54            "claude CLI not found in PATH. Install it from https://docs.anthropic.com/en/docs/claude-code"
55        );
56    }
57
58    #[test]
59    fn non_zero_exit_message() {
60        let err = ClaudeError::NonZeroExit {
61            code: 1,
62            stderr: "something went wrong".into(),
63        };
64        assert_eq!(
65            err.to_string(),
66            "claude exited with code 1: something went wrong"
67        );
68    }
69
70    #[test]
71    fn timeout_message() {
72        let err = ClaudeError::Timeout;
73        assert_eq!(err.to_string(), "request timed out");
74    }
75
76    #[test]
77    fn from_io_error() {
78        let io_err = io::Error::new(io::ErrorKind::Other, "disk full");
79        let err = ClaudeError::from(io_err);
80        assert!(matches!(err, ClaudeError::Io(_)));
81        assert_eq!(err.to_string(), "disk full");
82    }
83
84    #[test]
85    fn from_serde_error() {
86        let serde_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
87        let err = ClaudeError::from(serde_err);
88        assert!(matches!(err, ClaudeError::ParseError(_)));
89    }
90
91    #[test]
92    fn structured_output_error_message() {
93        let serde_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
94        let err = ClaudeError::StructuredOutputError {
95            raw_result: "raw text here".into(),
96            source: serde_err,
97        };
98        assert!(
99            err.to_string()
100                .starts_with("failed to deserialize structured output:")
101        );
102    }
103
104    #[test]
105    fn structured_output_error_preserves_raw_result() {
106        let serde_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
107        let err = ClaudeError::StructuredOutputError {
108            raw_result: "the raw output".into(),
109            source: serde_err,
110        };
111        match err {
112            ClaudeError::StructuredOutputError { raw_result, .. } => {
113                assert_eq!(raw_result, "the raw output");
114            }
115            _ => panic!("expected StructuredOutputError"),
116        }
117    }
118}