throw_error/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3
4//! A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
5//! that can be caught by user-defined error hooks.
6
7use std::{
8    cell::RefCell,
9    error,
10    fmt::{self, Display},
11    future::Future,
12    ops,
13    pin::Pin,
14    sync::Arc,
15    task::{Context, Poll},
16};
17
18/* Wrapper Types */
19
20/// A generic wrapper for any error.
21#[derive(Debug, Clone)]
22#[repr(transparent)]
23pub struct Error(Arc<dyn error::Error + Send + Sync>);
24
25impl Error {
26    /// Converts the wrapper into the inner reference-counted error.
27    pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
28        Arc::clone(&self.0)
29    }
30}
31
32impl ops::Deref for Error {
33    type Target = Arc<dyn error::Error + Send + Sync>;
34
35    fn deref(&self) -> &Self::Target {
36        &self.0
37    }
38}
39
40impl fmt::Display for Error {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}", self.0)
43    }
44}
45
46impl<T> From<T> for Error
47where
48    T: Into<Box<dyn error::Error + Send + Sync + 'static>>,
49{
50    fn from(value: T) -> Self {
51        Error(Arc::from(value.into()))
52    }
53}
54
55/// Implements behavior that allows for global or scoped error handling.
56///
57/// This allows for both "throwing" errors to register them, and "clearing" errors when they are no
58/// longer valid. This is useful for something like a user interface, in which an error can be
59/// "thrown" on some invalid user input, and later "cleared" if the user corrects the input.
60/// Keeping a unique identifier for each error allows the UI to be updated accordingly.
61pub trait ErrorHook: Send + Sync {
62    /// Handles the given error, returning a unique identifier.
63    fn throw(&self, error: Error) -> ErrorId;
64
65    /// Clears the error associated with the given identifier.
66    fn clear(&self, id: &ErrorId);
67}
68
69/// A unique identifier for an error. This is returned when you call [`throw`], which calls a
70/// global error handler.
71#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
72pub struct ErrorId(usize);
73
74impl Display for ErrorId {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        Display::fmt(&self.0, f)
77    }
78}
79
80impl From<usize> for ErrorId {
81    fn from(value: usize) -> Self {
82        Self(value)
83    }
84}
85
86thread_local! {
87    static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
88}
89
90/// Resets the error hook to its previous state when dropped.
91pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
92
93impl Drop for ResetErrorHookOnDrop {
94    fn drop(&mut self) {
95        ERROR_HOOK.with_borrow_mut(|this| *this = self.0.take())
96    }
97}
98
99/// Returns the current error hook.
100pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
101    ERROR_HOOK.with_borrow(Clone::clone)
102}
103
104/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
105pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
106    ResetErrorHookOnDrop(
107        ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
108    )
109}
110
111/// Invokes the error hook set by [`set_error_hook`] with the given error.
112pub fn throw(error: impl Into<Error>) -> ErrorId {
113    ERROR_HOOK
114        .with_borrow(|hook| hook.as_ref().map(|hook| hook.throw(error.into())))
115        .unwrap_or_default()
116}
117
118/// Clears the given error from the current error hook.
119pub fn clear(id: &ErrorId) {
120    ERROR_HOOK
121        .with_borrow(|hook| hook.as_ref().map(|hook| hook.clear(id)))
122        .unwrap_or_default()
123}
124
125pin_project_lite::pin_project! {
126    /// A [`Future`] that reads the error hook that is set when it is created, and sets this as the
127    /// current error hook whenever it is polled.
128    pub struct ErrorHookFuture<Fut> {
129        hook: Option<Arc<dyn ErrorHook>>,
130        #[pin]
131        inner: Fut
132    }
133}
134
135impl<Fut> ErrorHookFuture<Fut> {
136    /// Reads the current hook and wraps the given [`Future`], returning a new `Future` that will
137    /// set the error hook whenever it is polled.
138    pub fn new(inner: Fut) -> Self {
139        Self {
140            hook: ERROR_HOOK.with_borrow(Clone::clone),
141            inner,
142        }
143    }
144}
145
146impl<Fut> Future for ErrorHookFuture<Fut>
147where
148    Fut: Future,
149{
150    type Output = Fut::Output;
151
152    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
153        let this = self.project();
154        let _hook = this
155            .hook
156            .as_ref()
157            .map(|hook| set_error_hook(Arc::clone(hook)));
158        this.inner.poll(cx)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use std::error::Error as StdError;
166
167    #[derive(Debug)]
168    struct MyError;
169
170    impl Display for MyError {
171        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172            write!(f, "MyError")
173        }
174    }
175
176    impl StdError for MyError {}
177
178    #[test]
179    fn test_from() {
180        let e = MyError;
181        let _le = Error::from(e);
182
183        let e = "some error".to_string();
184        let _le = Error::from(e);
185
186        let e = anyhow::anyhow!("anyhow error");
187        let _le = Error::from(e);
188    }
189}