Skip to main content

restate_sdk/
errors.rs

1//! # Error Handling
2//!
3//! Restate handles retries for failed invocations.
4//! By default, Restate does infinite retries with an exponential backoff strategy.
5//!
6//! For failures for which you do not want retries, but instead want the invocation to end and the error message
7//! to be propagated back to the caller, you can return a [`TerminalError`].
8//!
9//! You can return a [`TerminalError`] with an optional HTTP status code and a message anywhere in your handler, as follows:
10//!
11//! ```rust,no_run
12//! # use restate_sdk::prelude::*;
13//! # async fn handle() -> Result<(), HandlerError> {
14//! Err(TerminalError::new("This is a terminal error").into())
15//! # }
16//! ```
17//!
18//! You can catch terminal exceptions. For example, you can catch the terminal exception that comes out of a [call to another service][crate::context::ContextClient], and build your control flow around it.
19//!
20//! ## Converting Errors to Terminal Errors
21//!
22//! The [`TerminalErrorExt`] trait provides a convenient way to convert any `Result` error
23//! into a terminal error using the `.terminal()` method:
24//!
25//! ```rust,no_run
26//! # use restate_sdk::prelude::*;
27//! # async fn handle() -> Result<(), HandlerError> {
28//! let parsed: i32 = "not a number".parse().terminal()?;
29//! # Ok(())
30//! # }
31//! ```
32use restate_sdk_shared_core::TerminalFailure;
33use std::error::Error as StdError;
34use std::fmt;
35
36#[derive(Debug)]
37pub(crate) enum HandlerErrorInner {
38    Retryable(Box<dyn StdError + Send + Sync + 'static>),
39    Terminal(TerminalErrorInner),
40}
41
42impl fmt::Display for HandlerErrorInner {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            HandlerErrorInner::Retryable(e) => {
46                write!(f, "Retryable error: {}", e)
47            }
48            HandlerErrorInner::Terminal(e) => fmt::Display::fmt(e, f),
49        }
50    }
51}
52
53impl StdError for HandlerErrorInner {
54    fn source(&self) -> Option<&(dyn StdError + 'static)> {
55        match self {
56            HandlerErrorInner::Retryable(e) => Some(e.as_ref()),
57            HandlerErrorInner::Terminal(e) => Some(e),
58        }
59    }
60}
61
62/// This error can contain either a [`TerminalError`], or any other Rust's [`StdError`].
63/// For the latter, the error is considered "retryable", and the execution will be retried.
64#[derive(Debug)]
65pub struct HandlerError(pub(crate) HandlerErrorInner);
66
67impl<E: Into<Box<dyn StdError + Send + Sync + 'static>>> From<E> for HandlerError {
68    fn from(value: E) -> Self {
69        Self(HandlerErrorInner::Retryable(value.into()))
70    }
71}
72
73impl From<TerminalError> for HandlerError {
74    fn from(value: TerminalError) -> Self {
75        Self(HandlerErrorInner::Terminal(value.0))
76    }
77}
78
79// Took from anyhow
80impl AsRef<dyn StdError + Send + Sync> for HandlerError {
81    fn as_ref(&self) -> &(dyn StdError + Send + Sync + 'static) {
82        &self.0
83    }
84}
85
86impl AsRef<dyn StdError> for HandlerError {
87    fn as_ref(&self) -> &(dyn StdError + 'static) {
88        &self.0
89    }
90}
91
92#[derive(Debug, Clone)]
93pub(crate) struct TerminalErrorInner {
94    code: u16,
95    message: String,
96}
97
98impl fmt::Display for TerminalErrorInner {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "Terminal error [{}]: {}", self.code, self.message)
101    }
102}
103
104impl StdError for TerminalErrorInner {}
105
106/// Error representing the result of an operation recorded in the journal.
107///
108/// When returned inside a [`crate::context::ContextSideEffects::run`] closure, or in a handler, it completes the operation with a failure value.
109#[derive(Debug, Clone)]
110pub struct TerminalError(pub(crate) TerminalErrorInner);
111
112impl TerminalError {
113    /// Create a new [`TerminalError`].
114    pub fn new(message: impl Into<String>) -> Self {
115        Self::new_with_code(500, message)
116    }
117
118    /// Create a new [`TerminalError`] with a status code.
119    pub fn new_with_code(code: u16, message: impl Into<String>) -> Self {
120        Self(TerminalErrorInner {
121            code,
122            message: message.into(),
123        })
124    }
125
126    /// Set the status code for this [`TerminalError`].
127    ///
128    /// ```rust,no_run
129    /// use restate_sdk::prelude::*;
130    ///
131    /// let error = TerminalError::new("Bad request").with_code(400);
132    /// assert_eq!(error.code(), 400);
133    /// ```
134    pub fn with_code(mut self, code: u16) -> Self {
135        self.0.code = code;
136        self
137    }
138
139    pub fn code(&self) -> u16 {
140        self.0.code
141    }
142
143    pub fn message(&self) -> &str {
144        &self.0.message
145    }
146
147    pub fn from_error<E: StdError>(e: E) -> Self {
148        Self::new(e.to_string())
149    }
150}
151
152impl fmt::Display for TerminalError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        fmt::Display::fmt(&self.0, f)
155    }
156}
157
158impl AsRef<dyn StdError + Send + Sync> for TerminalError {
159    fn as_ref(&self) -> &(dyn StdError + Send + Sync + 'static) {
160        &self.0
161    }
162}
163
164impl AsRef<dyn StdError> for TerminalError {
165    fn as_ref(&self) -> &(dyn StdError + 'static) {
166        &self.0
167    }
168}
169
170impl From<TerminalFailure> for TerminalError {
171    fn from(value: TerminalFailure) -> Self {
172        Self(TerminalErrorInner {
173            code: value.code,
174            message: value.message,
175        })
176    }
177}
178
179impl From<TerminalError> for TerminalFailure {
180    fn from(value: TerminalError) -> Self {
181        Self {
182            code: value.0.code,
183            message: value.0.message,
184            metadata: vec![],
185        }
186    }
187}
188
189/// Result type for a Restate handler.
190pub type HandlerResult<T> = Result<T, HandlerError>;
191
192/// Extension trait for converting any `Result` error into a [`TerminalError`].
193///
194/// This trait provides a convenient way to convert errors from fallible operations
195/// into terminal errors that will not be retried by Restate.
196///
197/// # Example
198///
199/// ```rust,no_run
200/// use restate_sdk::prelude::*;
201///
202/// async fn handle() -> Result<(), HandlerError> {
203///     let parsed: i32 = "not a number".parse().terminal()?;
204///     Ok(())
205/// }
206/// ```
207pub trait TerminalErrorExt<T, E> {
208    /// Convert the error into a [`TerminalError`] with the default status code (500).
209    fn terminal(self) -> Result<T, HandlerError>;
210}
211
212impl<T, E> TerminalErrorExt<T, E> for Result<T, E>
213where
214    E: std::fmt::Display + Send + Sync + 'static,
215{
216    fn terminal(self) -> Result<T, HandlerError> {
217        self.map_err(|err| TerminalError::new(err.to_string()).into())
218    }
219}