Skip to main content

swiftide_tasks/
errors.rs

1//! Error types used while defining and running task graphs.
2//!
3//! # Examples
4//!
5//! ```no_run
6//! use swiftide_tasks::{Task, TaskError};
7//!
8//! # #[tokio::main(flavor = "current_thread")]
9//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! let mut task = Task::<i32, i32>::new();
11//!
12//! let result = task.run(1).await;
13//! assert!(matches!(result, Err(TaskError::NoSteps)));
14//! # Ok(())
15//! # }
16//! ```
17use std::{any::Any, sync::Arc};
18
19use super::transition::Transition;
20
21/// Errors returned while defining, running, or resuming a task.
22#[derive(thiserror::Error, Debug)]
23pub enum TaskError {
24    /// A registered node failed while executing.
25    #[error(transparent)]
26    NodeError(#[from] NodeError),
27
28    /// A node was executed without an outgoing transition.
29    #[error("MissingTransition: {0}")]
30    MissingTransition(String),
31
32    /// A transition referenced a node that does not exist.
33    #[error("MissingNode: {0}")]
34    MissingNode(String),
35
36    /// The task produced a final output with the wrong type.
37    #[error("Task failed with wrong output")]
38    TypeError(String),
39
40    /// `run` was called while the task still had live runtime state.
41    #[error("Task already has active or paused work")]
42    TaskActive,
43
44    /// `resume` was called without paused or reset work to continue.
45    #[error("Task has no paused or queued work to resume")]
46    NotResumable,
47
48    /// The task drained all work without finishing or pausing.
49    #[error("Task ended without completing or pausing")]
50    Incomplete,
51
52    /// The task runtime detected an internal inconsistency.
53    #[error("Task entered an invalid internal state: {0}")]
54    InvalidState(String),
55
56    /// A node was executed without the required input.
57    #[error("MissingInput: {0}")]
58    MissingInput(String),
59
60    /// A node completed without the expected output.
61    #[error("MissingOutput: {0}")]
62    MissingOutput(String),
63
64    /// The task does not have a valid start node and transition graph.
65    #[error("Task is missing steps")]
66    NoSteps,
67}
68
69impl TaskError {
70    /// Creates a missing-transition error for `node_id`.
71    pub fn missing_transition(node_id: usize) -> Self {
72        TaskError::MissingTransition(format!("Node {node_id} is missing a transition"))
73    }
74
75    /// Creates a missing-node error for `node_id`.
76    pub fn missing_node(node_id: usize) -> Self {
77        TaskError::MissingNode(format!("Node {node_id} is missing"))
78    }
79
80    /// Creates a missing-input error for `node_id`.
81    pub fn missing_input(node_id: usize) -> Self {
82        TaskError::MissingInput(format!("Node {node_id} is missing input"))
83    }
84
85    /// Creates a missing-output error for `node_id`.
86    pub fn missing_output(node_id: usize) -> Self {
87        TaskError::MissingOutput(format!("Node {node_id} is missing output"))
88    }
89
90    /// Creates a type error describing the unexpected final output type.
91    pub fn type_error<T: Any + Send>(output: &T) -> Self {
92        let message = format!(
93            "Expected output of type {}, but got {:?}",
94            std::any::type_name::<T>(),
95            output.type_id()
96        );
97        TaskError::TypeError(message)
98    }
99
100    /// Creates an invalid-state error with a custom message.
101    pub fn invalid_state(message: impl Into<String>) -> Self {
102        TaskError::InvalidState(message.into())
103    }
104}
105
106/// Wraps an error produced by a specific node execution.
107#[derive(Debug, thiserror::Error)]
108pub struct NodeError {
109    /// The original node error.
110    pub node_error: Box<dyn std::error::Error + Send + Sync>,
111    /// The transition that would have been applied after this node, when available.
112    pub transition: Option<Arc<Transition>>,
113    /// The numeric identifier of the failing node.
114    pub node_id: usize,
115}
116
117impl std::fmt::Display for NodeError {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(
120            f,
121            "Node error in node {}: {}",
122            self.node_id, self.node_error
123        )
124    }
125}
126
127impl NodeError {
128    /// Creates a new node error tied to `node_id`.
129    pub fn new(
130        node_error: impl Into<Box<dyn std::error::Error + Send + Sync>>,
131        node_id: usize,
132        transition: Option<Transition>,
133    ) -> Self {
134        Self {
135            node_error: node_error.into(),
136            transition: transition.map(Arc::new),
137            node_id,
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::NodeError;
145
146    #[derive(Debug)]
147    struct ByteBackedError(Vec<u8>);
148
149    impl std::fmt::Display for ByteBackedError {
150        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151            formatter.write_str(&String::from_utf8_lossy(&self.0))
152        }
153    }
154
155    impl std::error::Error for ByteBackedError {}
156
157    #[test]
158    fn node_error_uses_inner_error_display() {
159        let error = NodeError::new(ByteBackedError(b"readable failure".to_vec()), 2, None);
160
161        assert_eq!(error.to_string(), "Node error in node 2: readable failure");
162    }
163}