Skip to main content

topcoat_core/
error.rs

1use std::{
2    any::Any,
3    backtrace::{Backtrace, BacktraceStatus},
4    fmt::{self, Debug, Display},
5    sync::Arc,
6};
7
8pub type Result<T = (), E = Error> = ::core::result::Result<T, E>;
9
10/// Error type used by Topcoat APIs.
11///
12/// Use `?` to convert a compatible [`std::error::Error`] into this type.
13/// Inspect its concrete value with [`downcast_ref`](Self::downcast_ref), or
14/// recover it with [`downcast`](Self::downcast). Clones share the stored error.
15/// Backtrace capture follows the environment settings described by
16/// [`backtrace`](Self::backtrace).
17///
18/// With the `anyhow` feature, `Error::from_anyhow` converts an `anyhow::Error`
19/// as well.
20#[derive(Clone)]
21pub struct Error(Arc<dyn ErrorObject>);
22
23impl Error {
24    /// Builds an error from a message.
25    pub fn msg(message: impl Display + Debug + Send + Sync + 'static) -> Self {
26        Self::from(Message(message))
27    }
28
29    /// Builds an error from a boxed error. Its message and sources carry
30    /// over, but the concrete error inside the box is not reachable through
31    /// the downcast methods.
32    #[must_use]
33    pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
34        Self::from(Boxed(error))
35    }
36
37    /// Builds an error from an [`anyhow::Error`]. Its message and sources
38    /// carry over, but the concrete error inside it is not reachable through
39    /// the downcast methods.
40    #[cfg(feature = "anyhow")]
41    #[must_use]
42    pub fn from_anyhow(error: anyhow::Error) -> Self {
43        Self::from_boxed(error.into())
44    }
45
46    /// Wraps this error in a context message, which becomes its [`Display`]
47    /// output. The wrapped error stays reachable through
48    /// [`source`](std::error::Error::source), [`chain`](Self::chain), and
49    /// the downcast methods.
50    #[must_use]
51    pub fn context(self, context: impl Display + Send + Sync + 'static) -> Self {
52        Self::from(WithContext {
53            context: Box::new(context),
54            error: self,
55        })
56    }
57
58    /// Returns whether the stored error is an instance of `E`, looking through
59    /// [`context`](Self::context) layers.
60    #[inline]
61    #[must_use]
62    pub fn is<E>(&self) -> bool
63    where
64        E: std::error::Error + Send + Sync + 'static,
65    {
66        self.downcast_ref::<E>().is_some()
67    }
68
69    /// Attempts to move the concrete error out of this error object, looking
70    /// through [`context`](Self::context) layers and discarding their
71    /// messages.
72    ///
73    /// This never clones the stored error, so it only succeeds while this is
74    /// the sole handle to it. To fall back to a clone instead, use
75    /// [`downcast_cloned`](Self::downcast_cloned).
76    ///
77    /// # Errors
78    ///
79    /// Returns a [`DowncastError`] carrying the original error back, and
80    /// saying whether the stored error is not an instance of `E` or a clone
81    /// of it is still alive.
82    pub fn downcast<E>(self) -> Result<E, DowncastError>
83    where
84        E: std::error::Error + Send + Sync + 'static,
85    {
86        if !self.is::<E>() {
87            return Err(DowncastError::new(DowncastFailure::Mismatch, self));
88        }
89        self.unwrap::<E>()
90    }
91
92    /// Attempts to downcast the error object to a concrete type, looking
93    /// through [`context`](Self::context) layers.
94    ///
95    /// The stored error is moved out when this is the sole handle to it and
96    /// cloned otherwise.
97    ///
98    /// # Errors
99    ///
100    /// Returns `Err(Self)` if the stored error is not an instance of `E`,
101    /// handing back the original error unchanged.
102    pub fn downcast_cloned<E>(self) -> Result<E, Self>
103    where
104        E: std::error::Error + Send + Sync + Clone + 'static,
105    {
106        match self.downcast::<E>() {
107            Ok(error) => Ok(error),
108            Err(failed) => {
109                let error = failed.into_error();
110                match error.downcast_ref::<E>() {
111                    Some(error) => Ok(error.clone()),
112                    None => Err(error),
113                }
114            }
115        }
116    }
117
118    /// Borrows the concrete error, looking through
119    /// [`context`](Self::context) layers.
120    #[must_use]
121    pub fn downcast_ref<E>(&self) -> Option<&E>
122    where
123        E: std::error::Error + Send + Sync + 'static,
124    {
125        let error = self.0.error();
126        match error.downcast_ref::<E>() {
127            Some(error) => Some(error),
128            None => error
129                .downcast_ref::<WithContext>()
130                .and_then(|context| context.error.downcast_ref::<E>()),
131        }
132    }
133
134    /// Mutably borrows the concrete error, looking through
135    /// [`context`](Self::context) layers.
136    ///
137    /// # Errors
138    ///
139    /// Returns a [`DowncastFailure`] saying whether the stored error is not
140    /// an instance of `E` or a clone of it is still alive: shared contents
141    /// cannot be handed out mutably.
142    pub fn downcast_mut<E>(&mut self) -> Result<&mut E, DowncastFailure>
143    where
144        E: std::error::Error + Send + Sync + 'static,
145    {
146        if !self.is::<E>() {
147            return Err(DowncastFailure::Mismatch);
148        }
149        self.unwrap_mut::<E>()
150    }
151
152    /// Moves the stored error out of an error known to hold an `E`, either
153    /// directly or behind context layers.
154    fn unwrap<E>(self) -> Result<E, DowncastError>
155    where
156        E: std::error::Error + Send + Sync + 'static,
157    {
158        let any = match self.0.into_any().downcast::<Object<E>>() {
159            Ok(object) => {
160                return match Arc::try_unwrap(object) {
161                    Ok(object) => Ok(object.error),
162                    Err(shared) => Err(DowncastError::new(DowncastFailure::Shared, Self(shared))),
163                };
164            }
165            Err(any) => any,
166        };
167        let Ok(object) = any.downcast::<Object<WithContext>>() else {
168            unreachable!("an error holding an `E` is an `Object<E>` or a context layer");
169        };
170        match Arc::try_unwrap(object) {
171            Ok(object) => {
172                let WithContext { context, error } = object.error;
173                error.unwrap::<E>().map_err(|failed| {
174                    // Put the layer back so the original error is handed back
175                    // intact, backtrace included.
176                    let error = Self(Arc::new(Object {
177                        error: WithContext {
178                            context,
179                            error: failed.error,
180                        },
181                        backtrace: object.backtrace,
182                    }));
183                    DowncastError::new(failed.failure, error)
184                })
185            }
186            Err(shared) => Err(DowncastError::new(DowncastFailure::Shared, Self(shared))),
187        }
188    }
189
190    /// Borrows the stored error of an error known to hold an `E`, either
191    /// directly or behind context layers.
192    fn unwrap_mut<E>(&mut self) -> Result<&mut E, DowncastFailure>
193    where
194        E: std::error::Error + Send + Sync + 'static,
195    {
196        let error = Arc::get_mut(&mut self.0)
197            .ok_or(DowncastFailure::Shared)?
198            .error_mut();
199        if error.is::<E>() {
200            return Ok(error
201                .downcast_mut::<E>()
202                .expect("the stored error was checked to be an `E`"));
203        }
204        error
205            .downcast_mut::<WithContext>()
206            .expect("an error holding an `E` is an `E` or a context layer")
207            .error
208            .unwrap_mut::<E>()
209    }
210
211    /// The backtrace captured when the error was built. Capture follows the
212    /// `RUST_BACKTRACE` and `RUST_LIB_BACKTRACE` environment variables; check
213    /// [`Backtrace::status`] before relying on its contents.
214    #[inline]
215    pub fn backtrace(&self) -> &Backtrace {
216        self.0.backtrace()
217    }
218
219    /// The stored error followed by each of its
220    /// [`source`](std::error::Error::source)s, outermost first.
221    pub fn chain(&self) -> impl Iterator<Item = &(dyn std::error::Error + 'static)> {
222        let mut next: Option<&(dyn std::error::Error + 'static)> = Some(self.0.error());
223        std::iter::from_fn(move || {
224            let current = next?;
225            next = current.source();
226            Some(current)
227        })
228    }
229}
230
231/// Prints the stored error's message; the alternate form (`{:#}`) appends
232/// each source after a colon.
233impl Display for Error {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        Display::fmt(self.0.error(), f)?;
236        if f.alternate() {
237            for cause in self.chain().skip(1) {
238                write!(f, ": {cause}")?;
239            }
240        }
241        Ok(())
242    }
243}
244
245/// Prints the stored error's message, its sources, and the backtrace when one
246/// was captured; the alternate form (`{:#?}`) prints the stored error's own
247/// [`Debug`] output instead.
248impl Debug for Error {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        if f.alternate() {
251            return Debug::fmt(self.0.error(), f);
252        }
253        write!(f, "{}", self.0.error())?;
254        let mut causes = self.chain().skip(1).peekable();
255        if causes.peek().is_some() {
256            write!(f, "\n\nCaused by:")?;
257            for cause in causes {
258                write!(f, "\n    {cause}")?;
259            }
260        }
261        let backtrace = self.backtrace();
262        if backtrace.status() == BacktraceStatus::Captured {
263            write!(f, "\n\nStack backtrace:\n{backtrace}")?;
264        }
265        Ok(())
266    }
267}
268
269impl<E> From<E> for Error
270where
271    E: std::error::Error + Send + Sync + 'static,
272{
273    fn from(error: E) -> Self {
274        Self(Arc::new(Object {
275            error,
276            backtrace: Backtrace::capture(),
277        }))
278    }
279}
280
281impl From<Error> for Box<dyn std::error::Error + Send + Sync + 'static> {
282    fn from(error: Error) -> Self {
283        Box::new(BoxedError(error))
284    }
285}
286
287/// Why a downcast could not hand out the stored error.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum DowncastFailure {
290    /// The stored error is not an instance of the requested type.
291    Mismatch,
292    /// A clone of the error is still alive, so its contents can neither be
293    /// moved out nor borrowed mutably.
294    Shared,
295}
296
297impl Display for DowncastFailure {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        match self {
300            Self::Mismatch => f.write_str("the error is not an instance of the requested type"),
301            Self::Shared => f.write_str("the error is still shared with a clone"),
302        }
303    }
304}
305
306impl std::error::Error for DowncastFailure {}
307
308/// A failed [`Error::downcast`], carrying the original error back.
309///
310/// Deliberately not an error type itself: the only way past it is
311/// [`into_error`](Self::into_error), so the original error cannot be
312/// replaced by a shell describing the failure through `?` or `.into()`.
313#[derive(Debug)]
314pub struct DowncastError {
315    failure: DowncastFailure,
316    error: Error,
317}
318
319impl DowncastError {
320    fn new(failure: DowncastFailure, error: Error) -> Self {
321        Self { failure, error }
322    }
323
324    /// Why the downcast failed.
325    #[must_use]
326    pub fn failure(&self) -> DowncastFailure {
327        self.failure
328    }
329
330    /// The error the downcast was attempted on.
331    #[must_use]
332    pub fn error(&self) -> &Error {
333        &self.error
334    }
335
336    /// Hands the original error back.
337    #[must_use]
338    pub fn into_error(self) -> Error {
339        self.error
340    }
341}
342
343/// The object an [`Error`] points at: a concrete error together with the
344/// backtrace captured when it was wrapped.
345///
346/// A trait object rather than `dyn std::error::Error` so the error can be
347/// handed back by value: [`into_any`](Self::into_any) recovers the concrete
348/// object, which [`Arc::try_unwrap`] then moves out of.
349trait ErrorObject: Send + Sync + 'static {
350    /// The stored error.
351    fn error(&self) -> &(dyn std::error::Error + Send + Sync + 'static);
352
353    /// The stored error, mutably.
354    fn error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static);
355
356    /// The backtrace captured when the error was wrapped.
357    fn backtrace(&self) -> &Backtrace;
358
359    /// Erases the object to `dyn Any`, from which its concrete type can be
360    /// recovered with [`Arc::downcast`].
361    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
362}
363
364/// The concrete [`ErrorObject`] holding an error of type `E`.
365struct Object<E> {
366    error: E,
367    backtrace: Backtrace,
368}
369
370impl<E> ErrorObject for Object<E>
371where
372    E: std::error::Error + Send + Sync + 'static,
373{
374    fn error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
375        &self.error
376    }
377
378    fn error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
379        &mut self.error
380    }
381
382    fn backtrace(&self) -> &Backtrace {
383        &self.backtrace
384    }
385
386    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
387        self
388    }
389}
390
391/// The error behind [`Error::msg`].
392struct Message<M>(M);
393
394impl<M: Display> Display for Message<M> {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        Display::fmt(&self.0, f)
397    }
398}
399
400impl<M: Debug> Debug for Message<M> {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        Debug::fmt(&self.0, f)
403    }
404}
405
406impl<M: Display + Debug> std::error::Error for Message<M> {}
407
408/// The error behind [`Error::context`]: a message in front of the error it
409/// wraps. Not generic over the message so a downcast can look through it.
410struct WithContext {
411    context: Box<dyn Display + Send + Sync>,
412    error: Error,
413}
414
415impl Display for WithContext {
416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        Display::fmt(&self.context, f)
418    }
419}
420
421impl Debug for WithContext {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        f.debug_struct("WithContext")
424            .field("context", &format_args!("{}", self.context))
425            .field("error", &self.error)
426            .finish()
427    }
428}
429
430impl std::error::Error for WithContext {
431    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
432        Some(self.error.0.error())
433    }
434}
435
436/// The error behind [`Error::from_boxed`], since a box of a trait object is
437/// not itself an [`std::error::Error`].
438struct Boxed(Box<dyn std::error::Error + Send + Sync + 'static>);
439
440impl Display for Boxed {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        Display::fmt(&self.0, f)
443    }
444}
445
446impl Debug for Boxed {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        Debug::fmt(&self.0, f)
449    }
450}
451
452impl std::error::Error for Boxed {
453    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
454        self.0.source()
455    }
456}
457
458/// An [`Error`] in the shape of an [`std::error::Error`], for conversion
459/// into a boxed error. [`Error`] cannot implement the trait itself: the
460/// blanket conversion from every [`std::error::Error`] would then apply to
461/// it and collide with the reflexive `From<Error> for Error`.
462struct BoxedError(Error);
463
464impl Display for BoxedError {
465    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466        Display::fmt(&self.0, f)
467    }
468}
469
470impl Debug for BoxedError {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        Debug::fmt(&self.0, f)
473    }
474}
475
476impl std::error::Error for BoxedError {
477    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
478        self.0.0.error().source()
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    /// A payload type to recover from wrapped errors.
487    #[derive(Debug, Clone)]
488    struct Failure(&'static str);
489
490    impl Display for Failure {
491        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492            f.write_str(self.0)
493        }
494    }
495
496    impl std::error::Error for Failure {}
497
498    /// A payload type with a source.
499    #[derive(Debug)]
500    struct WithCause {
501        cause: Failure,
502    }
503
504    impl Display for WithCause {
505        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506            f.write_str("outer")
507        }
508    }
509
510    impl std::error::Error for WithCause {
511        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
512            Some(&self.cause)
513        }
514    }
515
516    #[test]
517    fn the_error_is_two_words_wide() {
518        assert_eq!(size_of::<Error>(), 2 * size_of::<usize>());
519        assert_eq!(size_of::<Result<()>>(), 2 * size_of::<usize>());
520    }
521
522    #[test]
523    fn clone_shares_the_underlying_error() {
524        let error = Error::from(Failure("boom"));
525        let clone = error.clone();
526
527        assert_eq!(error.to_string(), "boom");
528        assert_eq!(clone.to_string(), "boom");
529        assert_eq!(
530            clone.downcast_ref::<Failure>().map(|failure| failure.0),
531            Some("boom")
532        );
533    }
534
535    #[test]
536    fn msg_builds_an_error_from_a_message() {
537        let error = Error::msg(format!("boom {}", 1));
538        assert_eq!(error.to_string(), "boom 1");
539        assert!(error.is::<Message<String>>());
540    }
541
542    #[test]
543    fn downcast_extracts_a_unique_error() {
544        let failure = Error::from(Failure("boom")).downcast::<Failure>().unwrap();
545        assert_eq!(failure.0, "boom");
546    }
547
548    #[test]
549    fn downcast_reports_a_mismatch() {
550        let error = Error::from(std::io::Error::other("boom"));
551        let failed = error.downcast::<Failure>().unwrap_err();
552        assert_eq!(failed.failure(), DowncastFailure::Mismatch);
553        assert_eq!(failed.into_error().to_string(), "boom");
554    }
555
556    #[test]
557    fn downcast_reports_a_shared_error() {
558        let error = Error::from(Failure("boom"));
559        let clone = error.clone();
560
561        let failed = error.downcast::<Failure>().unwrap_err();
562        assert_eq!(failed.failure(), DowncastFailure::Shared);
563        let error = failed.into_error();
564        assert_eq!(error.to_string(), "boom");
565
566        // Dropping the share makes the error unique again.
567        drop(clone);
568        let failure = error.downcast::<Failure>().unwrap();
569        assert_eq!(failure.0, "boom");
570    }
571
572    #[test]
573    fn downcast_moves_out_of_a_context_layer() {
574        let error = Error::from(Failure("boom")).context("loading");
575        let failure = error.downcast::<Failure>().unwrap();
576        assert_eq!(failure.0, "boom");
577    }
578
579    #[test]
580    fn downcast_hands_a_context_layer_back_intact() {
581        let inner = Error::from(Failure("boom"));
582        let shared = inner.clone();
583        let error = inner.context("loading");
584
585        let failed = error.downcast::<Failure>().unwrap_err();
586        assert_eq!(failed.failure(), DowncastFailure::Shared);
587        let error = failed.into_error();
588        assert_eq!(format!("{error:#}"), "loading: boom");
589        drop(shared);
590    }
591
592    #[test]
593    fn downcast_cloned_extracts_a_unique_error() {
594        let failure = Error::from(Failure("boom"))
595            .downcast_cloned::<Failure>()
596            .unwrap();
597        assert_eq!(failure.0, "boom");
598    }
599
600    #[test]
601    fn downcast_cloned_clones_a_shared_error() {
602        let error = Error::from(Failure("boom"));
603        let clone = error.clone();
604
605        let failure = error.downcast_cloned::<Failure>().unwrap();
606        assert_eq!(failure.0, "boom");
607        assert_eq!(clone.to_string(), "boom");
608    }
609
610    #[test]
611    fn downcast_cloned_keeps_a_non_matching_error() {
612        let error = Error::from(std::io::Error::other("boom"));
613        let clone = error.clone();
614
615        let error = error.downcast_cloned::<Failure>().unwrap_err();
616        assert_eq!(error.to_string(), "boom");
617        drop(clone);
618    }
619
620    #[test]
621    fn downcast_mut_mutates_a_unique_error() {
622        let mut error = Error::from(Failure("boom"));
623        error.downcast_mut::<Failure>().unwrap().0 = "bang";
624        assert_eq!(error.to_string(), "bang");
625    }
626
627    #[test]
628    fn downcast_mut_mutates_through_a_context_layer() {
629        let mut error = Error::from(Failure("boom")).context("loading");
630        error.downcast_mut::<Failure>().unwrap().0 = "bang";
631        assert_eq!(format!("{error:#}"), "loading: bang");
632    }
633
634    #[test]
635    fn downcast_mut_reports_a_mismatch() {
636        let mut error = Error::from(std::io::Error::other("boom"));
637        assert_eq!(
638            error.downcast_mut::<Failure>().unwrap_err(),
639            DowncastFailure::Mismatch
640        );
641    }
642
643    #[test]
644    fn downcast_mut_reports_a_shared_error() {
645        let mut error = Error::from(Failure("boom"));
646        let clone = error.clone();
647
648        assert_eq!(
649            error.downcast_mut::<Failure>().unwrap_err(),
650            DowncastFailure::Shared
651        );
652
653        drop(clone);
654        assert!(error.downcast_mut::<Failure>().is_ok());
655    }
656
657    #[test]
658    fn context_wraps_the_message_and_keeps_the_error_reachable() {
659        let error = Error::from(Failure("boom")).context("loading");
660
661        assert_eq!(error.to_string(), "loading");
662        assert_eq!(format!("{error:#}"), "loading: boom");
663        assert_eq!(error.chain().count(), 2);
664        assert!(error.is::<Failure>());
665        assert_eq!(error.downcast_cloned::<Failure>().unwrap().0, "boom");
666    }
667
668    #[test]
669    fn display_and_debug_walk_the_chain() {
670        let error = Error::from(WithCause {
671            cause: Failure("boom"),
672        });
673
674        assert_eq!(error.to_string(), "outer");
675        assert_eq!(format!("{error:#}"), "outer: boom");
676        assert!(format!("{error:?}").starts_with("outer\n\nCaused by:\n    boom"));
677        assert!(format!("{error:#?}").starts_with("WithCause"));
678    }
679
680    #[test]
681    fn box_conversion_keeps_the_message_and_source() {
682        let error = Error::from(WithCause {
683            cause: Failure("boom"),
684        });
685        let shared = error.clone();
686
687        let boxed: Box<dyn std::error::Error + Send + Sync> = error.into();
688        assert_eq!(boxed.to_string(), "outer");
689        assert_eq!(
690            boxed.source().map(ToString::to_string),
691            Some(String::from("boom"))
692        );
693        drop(shared);
694    }
695
696    #[test]
697    fn boxed_errors_convert_with_their_chain() {
698        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(WithCause {
699            cause: Failure("boom"),
700        });
701        let error = Error::from_boxed(boxed);
702        assert_eq!(error.to_string(), "outer");
703        assert_eq!(format!("{error:#}"), "outer: boom");
704    }
705
706    #[cfg(feature = "anyhow")]
707    #[test]
708    fn anyhow_errors_convert_with_their_chain() {
709        let error = Error::from_anyhow(anyhow::anyhow!("boom").context("loading"));
710        assert_eq!(error.to_string(), "loading");
711        assert_eq!(format!("{error:#}"), "loading: boom");
712    }
713}