Skip to main content

query_flow/
error.rs

1//! Error types for query execution.
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::ops::Deref;
6use std::sync::Arc;
7
8use crate::asset::PendingAsset;
9use crate::key::FullCacheKey;
10
11/// Query errors including both system-level and user errors.
12///
13/// User errors can be propagated using the `?` operator, which automatically
14/// converts any `Into<anyhow::Error>` type into `QueryError::UserError`.
15#[derive(Debug, Clone)]
16pub enum QueryError {
17    /// Query is waiting for async loading to complete.
18    ///
19    /// This is returned when a dependency is still loading via a background task.
20    /// Use `runtime.query_async()` to wait for loading to complete, or handle
21    /// explicitly in your query logic.
22    ///
23    /// The `asset` field contains information about the pending asset, which can
24    /// be downcast to the original key type using `asset.key::<K>()`.
25    Suspend {
26        /// The pending asset that caused the suspension.
27        asset: PendingAsset,
28    },
29
30    /// Dependency cycle detected.
31    ///
32    /// The query graph contains a cycle, which would cause infinite recursion.
33    /// The `path` contains the cache keys forming the cycle, which may include
34    /// both queries and assets when asset locators are involved.
35    Cycle {
36        /// The cache keys forming the cycle.
37        path: Vec<FullCacheKey>,
38    },
39
40    /// Query execution was cancelled.
41    Cancelled,
42
43    /// Dependencies were removed during query execution.
44    ///
45    /// This can happen if another thread removes queries or assets
46    /// while this query is being registered.
47    DependenciesRemoved {
48        /// Keys that were not found during registration.
49        missing_keys: Vec<FullCacheKey>,
50    },
51
52    /// Asset resolution occurred during query execution.
53    ///
54    /// This error is returned when `resolve_asset` is called while a query is
55    /// executing, and the resolved asset affects a dependency that the query
56    /// has already accessed. This would cause different parts of the query
57    /// to observe different asset values, violating consistency.
58    InconsistentAssetResolution,
59
60    /// User-defined error.
61    ///
62    /// This variant allows user errors to be propagated through the query system
63    /// using the `?` operator. Any type implementing `Into<anyhow::Error>` can be
64    /// converted to this variant.
65    ///
66    /// Unlike system errors (Suspend, Cycle, etc.), UserError results are cached
67    /// and participate in early cutoff optimization.
68    UserError(Arc<anyhow::Error>),
69}
70
71impl fmt::Display for QueryError {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            QueryError::Suspend { asset } => {
75                write!(f, "query suspended: waiting for {}", asset.debug_repr())
76            }
77            QueryError::Cycle { path } => {
78                let path_str: Vec<String> = path.iter().map(|k| k.debug_repr()).collect();
79                write!(f, "dependency cycle detected: {}", path_str.join(" -> "))
80            }
81            QueryError::Cancelled => write!(f, "query cancelled"),
82            QueryError::DependenciesRemoved { missing_keys } => {
83                write!(
84                    f,
85                    "dependencies removed during execution: {:?}",
86                    missing_keys
87                )
88            }
89            QueryError::InconsistentAssetResolution => {
90                write!(
91                    f,
92                    "asset resolution occurred during query execution, causing inconsistent snapshot"
93                )
94            }
95            QueryError::UserError(e) => write!(f, "user error: {}", e),
96        }
97    }
98}
99
100impl<T: Into<anyhow::Error>> From<T> for QueryError {
101    fn from(err: T) -> Self {
102        QueryError::UserError(Arc::new(err.into()))
103    }
104}
105
106impl QueryError {
107    /// Returns a reference to the inner user error if this is a `UserError` variant.
108    pub fn user_error(&self) -> Option<&Arc<anyhow::Error>> {
109        match self {
110            QueryError::UserError(e) => Some(e),
111            _ => None,
112        }
113    }
114
115    /// Attempts to downcast the user error to a specific type.
116    ///
117    /// Returns `Some(&E)` if this is a `UserError` containing an error of type `E`,
118    /// otherwise returns `None`.
119    pub fn downcast_ref<E: std::error::Error + Send + Sync + 'static>(&self) -> Option<&E> {
120        self.user_error().and_then(|e| e.downcast_ref::<E>())
121    }
122
123    /// Returns `true` if this is a `UserError` containing an error of type `E`.
124    pub fn is<E: std::error::Error + Send + Sync + 'static>(&self) -> bool {
125        self.downcast_ref::<E>().is_some()
126    }
127}
128
129/// A typed wrapper around a user error that provides `Deref` access to the inner error type.
130///
131/// This struct holds an `Arc<anyhow::Error>` internally and provides safe access to
132/// the downcasted error reference. The `Arc` ensures the error remains valid for the
133/// lifetime of this wrapper.
134///
135/// # Example
136///
137/// ```
138/// use std::fmt;
139///
140/// use query_flow::{query, Db, QueryError, QueryResultExt, QueryRuntime};
141///
142/// #[derive(Debug)]
143/// struct MyError {
144///     code: u32,
145/// }
146///
147/// impl fmt::Display for MyError {
148///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149///         write!(f, "my error {}", self.code)
150///     }
151/// }
152///
153/// impl std::error::Error for MyError {}
154///
155/// #[query]
156/// fn failing(db: &impl Db) -> Result<i32, QueryError> {
157///     let _ = db;
158///     Err(MyError { code: 42 }.into())
159/// }
160///
161/// #[query]
162/// fn caller(db: &impl Db) -> Result<String, QueryError> {
163///     let result = db.query(Failing::new()).downcast_err::<MyError>()?;
164///     Ok(match result {
165///         Ok(value) => format!("success: {}", value),
166///         Err(typed_err) => {
167///             // typed_err derefs to &MyError
168///             format!("Error code: {}", typed_err.code)
169///         }
170///     })
171/// }
172///
173/// let runtime = QueryRuntime::new();
174/// assert_eq!(*runtime.query(Caller::new()).unwrap(), "Error code: 42");
175/// ```
176#[derive(Clone)]
177pub struct TypedErr<E> {
178    arc: Arc<anyhow::Error>,
179    _marker: PhantomData<E>,
180}
181
182impl<E: std::error::Error + Send + Sync + 'static> TypedErr<E> {
183    fn new(arc: Arc<anyhow::Error>) -> Option<Self> {
184        // Verify the downcast is valid before constructing
185        if arc.downcast_ref::<E>().is_some() {
186            Some(Self {
187                arc,
188                _marker: PhantomData,
189            })
190        } else {
191            None
192        }
193    }
194
195    /// Returns a reference to the inner error.
196    pub fn get(&self) -> &E {
197        // Safe because we verified the type in `new`
198        self.arc.downcast_ref::<E>().unwrap()
199    }
200}
201
202impl<E> From<TypedErr<E>> for QueryError {
203    fn from(err: TypedErr<E>) -> Self {
204        QueryError::UserError(err.arc)
205    }
206}
207
208impl<E: std::error::Error + Send + Sync + 'static> Deref for TypedErr<E> {
209    type Target = E;
210
211    fn deref(&self) -> &E {
212        self.get()
213    }
214}
215
216impl<E: std::error::Error + Send + Sync + 'static> fmt::Debug for TypedErr<E> {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        fmt::Debug::fmt(self.get(), f)
219    }
220}
221
222impl<E: std::error::Error + Send + Sync + 'static> fmt::Display for TypedErr<E> {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        fmt::Display::fmt(self.get(), f)
225    }
226}
227
228/// Extension trait for query results that provides ergonomic error downcasting.
229///
230/// This trait is implemented for `Result<Arc<T>, QueryError>` and allows you to
231/// downcast user errors to a specific type while propagating system errors.
232///
233/// # Example
234///
235/// ```
236/// use std::fmt;
237///
238/// use query_flow::{query, Db, QueryError, QueryResultExt, QueryRuntime};
239///
240/// #[derive(Debug)]
241/// struct MyError {
242///     code: u32,
243/// }
244///
245/// impl fmt::Display for MyError {
246///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247///         write!(f, "my error {}", self.code)
248///     }
249/// }
250///
251/// impl std::error::Error for MyError {}
252///
253/// #[query]
254/// fn my_query(db: &impl Db, succeed: bool) -> Result<i32, QueryError> {
255///     let _ = db;
256///     if succeed {
257///         Ok(1)
258///     } else {
259///         Err(MyError { code: 7 }.into())
260///     }
261/// }
262///
263/// #[query]
264/// fn caller(db: &impl Db, succeed: bool) -> Result<String, QueryError> {
265///     // Downcast to MyError, propagating system errors and non-matching user errors
266///     let result = db.query(MyQuery::new(succeed)).downcast_err::<MyError>()?;
267///
268///     Ok(match result {
269///         Ok(value) => format!("Success: {:?}", value),
270///         Err(my_err) => format!("MyError: {}", my_err.code),
271///     })
272/// }
273///
274/// let runtime = QueryRuntime::new();
275/// assert_eq!(*runtime.query(Caller::new(true)).unwrap(), "Success: 1");
276/// assert_eq!(*runtime.query(Caller::new(false)).unwrap(), "MyError: 7");
277/// ```
278pub trait QueryResultExt<T> {
279    /// Attempts to downcast a `UserError` to a specific error type.
280    ///
281    /// # Returns
282    ///
283    /// - `Ok(Ok(value))` - The query succeeded with `value`
284    /// - `Ok(Err(typed_err))` - The query failed with a `UserError` of type `E`
285    /// - `Err(query_error)` - The query failed with a system error, or a `UserError`
286    ///   that is not of type `E`
287    ///
288    /// # Example
289    ///
290    /// ```
291    /// use std::fmt;
292    ///
293    /// use query_flow::{query, Db, QueryError, QueryResultExt, QueryRuntime};
294    ///
295    /// #[derive(Debug)]
296    /// struct MyError {
297    ///     message: String,
298    /// }
299    ///
300    /// impl fmt::Display for MyError {
301    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302    ///         write!(f, "{}", self.message)
303    ///     }
304    /// }
305    ///
306    /// impl std::error::Error for MyError {}
307    ///
308    /// #[query]
309    /// fn my_query(db: &impl Db) -> Result<i32, QueryError> {
310    ///     let _ = db;
311    ///     Err(MyError { message: "boom".into() }.into())
312    /// }
313    ///
314    /// #[query]
315    /// fn caller(db: &impl Db) -> Result<i32, QueryError> {
316    ///     // Handle specific error type, propagate others
317    ///     let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
318    ///     let value = result.map_err(|e| {
319    ///         eprintln!("MyError occurred: {}", e.message);
320    ///         e
321    ///     })?;
322    ///     Ok(*value)
323    /// }
324    ///
325    /// let runtime = QueryRuntime::new();
326    /// let err = runtime.query(Caller::new()).unwrap_err();
327    /// assert!(err.is::<MyError>());
328    /// ```
329    fn downcast_err<E: std::error::Error + Send + Sync + 'static>(
330        self,
331    ) -> Result<Result<Arc<T>, TypedErr<E>>, QueryError>;
332}
333
334impl<T> QueryResultExt<T> for Result<Arc<T>, QueryError> {
335    fn downcast_err<E: std::error::Error + Send + Sync + 'static>(
336        self,
337    ) -> Result<Result<Arc<T>, TypedErr<E>>, QueryError> {
338        match self {
339            Ok(value) => Ok(Ok(value)),
340            Err(QueryError::UserError(arc)) => match TypedErr::new(arc.clone()) {
341                Some(typed) => Ok(Err(typed)),
342                None => Err(QueryError::UserError(arc)),
343            },
344            Err(other) => Err(other),
345        }
346    }
347}