Skip to main content

runifold_effect/
error.rs

1use runifold_core::{JournalError, RunError};
2use thiserror::Error;
3
4/// Normalized effect-coordination failure category.
5#[derive(Clone, Debug, Eq, PartialEq)]
6#[non_exhaustive]
7pub enum EffectExecutorErrorKind {
8    /// The owning Run lacks the required capability.
9    CapabilityDenied,
10    /// An idempotency key was reused for different work.
11    IdempotencyConflict,
12    /// Recovery cannot prove that retry is safe.
13    Ambiguous,
14    /// The effect store rejected an operation.
15    Store,
16    /// Structured event recording failed.
17    Observability,
18    /// The handler failed.
19    Handler,
20    /// Execution was cancelled.
21    Cancelled,
22    /// The effective deadline elapsed.
23    DeadlineExceeded,
24    /// Stored state violated the protocol.
25    Protocol,
26}
27
28/// Structured failure from effect coordination.
29#[derive(Clone, Debug, Error, PartialEq)]
30#[error("{kind:?}: {message}")]
31pub struct EffectExecutorError {
32    /// Normalized category.
33    pub kind: EffectExecutorErrorKind,
34    /// Safe failure explanation.
35    pub message: String,
36    /// Original handler error, when applicable.
37    #[source]
38    pub source_error: Option<RunError>,
39}
40
41impl EffectExecutorError {
42    /// Creates an error without a handler source.
43    pub fn new(kind: EffectExecutorErrorKind, message: impl Into<String>) -> Self {
44        Self {
45            kind,
46            message: message.into(),
47            source_error: None,
48        }
49    }
50
51    pub(crate) fn handler(error: RunError) -> Self {
52        Self {
53            kind: EffectExecutorErrorKind::Handler,
54            message: error.to_string(),
55            source_error: Some(error),
56        }
57    }
58}
59
60impl From<JournalError> for EffectExecutorError {
61    fn from(error: JournalError) -> Self {
62        Self::new(EffectExecutorErrorKind::Observability, error.to_string())
63    }
64}