Skip to main content

traceable_error/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4//! Deterministic Error Tracing Architecture (`traceable_error`)
5//!
6//! This crate provides the foundational traits and compile-time hashing mechanisms
7//! required to establish a deterministic, traceable error hierarchy across distributed
8//! systems and multi-layered software architectures (such as Fuchsia and `ffx`).
9//!
10//! ## Overview
11//!
12//! When dealing with deeply nested software stacks or distributed IPC boundaries
13//! (e.g. FIDL, Overnet), errors often undergo type erasure or stringification. This crate
14//! establishes a mechanism where each distinct error variant across independent crates
15//! is assigned a stable string-based layer code in the format:
16//! `{crate_name}::{enum_name}::{variant_name}`.
17//!
18//! By chaining these layer codes chronologically, diagnostic systems can reconstruct the exact
19//! trajectory of a failure without relying on brittle string parsing or runtime type metadata.
20
21/// Defines an error that can be deterministically traced through a distributed architecture.
22///
23/// Implementations of this trait (typically derived automatically via `#[derive(TraceableError)]`
24/// on enums) are capable of recursively interrogating their underlying causal chain
25/// and reporting a unified chronological history of layer codes.
26///
27/// Each layer code is represented by a structured `String` identifier:
28/// `format!("{crate_name}::{enum_name}::{variant_name}")`.
29///
30/// This structured layout facilitates highly readable failure trajectory reconstruction
31/// across distributed IPC boundaries and dynamic crate boundaries.
32pub trait TraceableError: std::fmt::Debug + std::fmt::Display + 'static {
33    /// Downcasts this trait object to a concrete type.
34    fn as_any(&self) -> &dyn std::any::Any;
35    /// Returns this specific layer's string identifier (format: CrateName::EnumName::EnumValue).
36    fn layer_code(&self) -> String;
37
38    /// Recursively interrogates underlying error sources to build the chronological array of layer codes.
39    ///
40    /// The resulting vector is ordered from outermost (most recent) layer to innermost (root cause).
41    fn chain_codes(&self) -> Vec<String>;
42
43    /// Formats the layer code vector into a standardized diagnostic string (e.g., `"Crate1::Enum1::Val1-Crate2::Enum2::Val2"`).
44    fn diagnostic_code(&self) -> String {
45        self.chain_codes().join("-")
46    }
47
48    /// Returns the underlying causal error, if any, as a dynamic `std::error::Error`.
49    ///
50    /// If your type also implements `std::error::Error`, you should override this
51    /// to return `self.source()`. The `#[derive(TraceableError)]` macro does this automatically.
52    fn source_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
53        None
54    }
55}
56
57impl TraceableError for anyhow::Error {
58    fn as_any(&self) -> &dyn std::any::Any {
59        self
60    }
61
62    fn layer_code(&self) -> String {
63        if let Some(boxed) = self.downcast_ref::<TraceableBox>() {
64            boxed.layer_code()
65        } else {
66            "anyhow".to_string()
67        }
68    }
69
70    fn chain_codes(&self) -> Vec<String> {
71        if let Some(boxed) = self.downcast_ref::<TraceableBox>() {
72            // If the anyhow::Error contains a TraceableBox, we traverse into it.
73            // This intentionally bypasses the "anyhow" type-erasing transport layer
74            // to focus on the semantic concrete error chain.
75            boxed.chain_codes()
76        } else {
77            vec!["anyhow".to_string()]
78        }
79    }
80
81    fn source_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
82        if let Some(boxed) = self.downcast_ref::<TraceableBox>() {
83            std::error::Error::source(boxed)
84        } else {
85            self.source()
86        }
87    }
88}
89
90/// A concrete, sized encapsulation of a dynamic `TraceableError` trait object.
91///
92/// This wrapper acts as a type-erased boundary. It enables seamless bidirectional `?` operator
93/// compatibility across dynamic crate boundaries, allowing concrete error enums (via `thiserror`)
94/// and untyped conduits (`anyhow`) to nest inside each other without losing causal tracing history.
95///
96/// ## Example
97///
98/// ```rust
99/// use traceable_error::{TraceableError, TraceableBox};
100///
101/// fn produce_anyhow() -> anyhow::Result<()> {
102///     Err(anyhow::anyhow!("root failure"))
103/// }
104///
105/// // Seamlessly converts the anyhow::Error into a TraceableBox trait object via ?
106/// fn consume_box() -> Result<(), TraceableBox> {
107///     produce_anyhow()?;
108///     Ok(())
109/// }
110/// ```
111// Note: TraceableBox intentionally does NOT implement TraceableError.
112// This prevents double-boxing (e.g., wrapping a TraceableBox inside another TraceableBox)
113// at compile time, as it will fail the `E: TraceableError` bound in the `From` implementation.
114#[derive(Debug)]
115pub struct TraceableBox(pub Box<dyn TraceableError + Send + Sync + 'static>);
116
117impl<E: TraceableError + Send + Sync + 'static> From<E> for TraceableBox {
118    fn from(err: E) -> Self {
119        TraceableBox(Box::new(err))
120    }
121}
122
123impl TraceableBox {
124    pub fn as_any(&self) -> &dyn std::any::Any {
125        self.0.as_any()
126    }
127
128    pub fn layer_code(&self) -> String {
129        self.0.layer_code()
130    }
131
132    pub fn chain_codes(&self) -> Vec<String> {
133        self.0.chain_codes()
134    }
135
136    pub fn diagnostic_code(&self) -> String {
137        self.0.diagnostic_code()
138    }
139}
140
141impl std::fmt::Display for TraceableBox {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        write!(f, "{}", self.0)
144    }
145}
146
147impl std::error::Error for TraceableBox {
148    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
149        self.0.source_error()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[derive(Debug)]
158    struct DummyError;
159    impl std::fmt::Display for DummyError {
160        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161            write!(f, "DummyError")
162        }
163    }
164    impl TraceableError for DummyError {
165        fn as_any(&self) -> &dyn std::any::Any {
166            self
167        }
168        fn layer_code(&self) -> String {
169            "DummyError".to_string()
170        }
171        fn chain_codes(&self) -> Vec<String> {
172            vec![self.layer_code()]
173        }
174    }
175
176    #[test]
177    fn test_traceable_error() {
178        let _err = DummyError;
179    }
180
181    #[test]
182    fn test_traceable_box_display_delegates() {
183        #[derive(Debug)]
184        struct InnerError;
185        impl std::fmt::Display for InnerError {
186            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187                write!(f, "root failure")
188            }
189        }
190        impl TraceableError for InnerError {
191            fn as_any(&self) -> &dyn std::any::Any {
192                self
193            }
194            fn layer_code(&self) -> String {
195                "Inner".to_string()
196            }
197            fn chain_codes(&self) -> Vec<String> {
198                vec![self.layer_code()]
199            }
200        }
201
202        #[derive(Debug)]
203        struct OuterError(TraceableBox);
204        impl std::fmt::Display for OuterError {
205            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206                write!(f, "outer wrapper: {}", self.0)
207            }
208        }
209        impl TraceableError for OuterError {
210            fn as_any(&self) -> &dyn std::any::Any {
211                self
212            }
213            fn layer_code(&self) -> String {
214                "Outer".to_string()
215            }
216            fn chain_codes(&self) -> Vec<String> {
217                let mut c = self.0.chain_codes();
218                c.insert(0, self.layer_code());
219                c
220            }
221        }
222
223        let inner_box = TraceableBox::from(InnerError);
224        assert_eq!(inner_box.to_string(), "root failure");
225        assert_eq!(inner_box.diagnostic_code(), "Inner");
226
227        let outer_box = TraceableBox::from(OuterError(inner_box));
228        assert_eq!(outer_box.to_string(), "outer wrapper: root failure");
229        assert_eq!(outer_box.diagnostic_code(), "Outer-Inner");
230    }
231
232    #[test]
233    fn test_anyhow_traceable() {
234        let err = anyhow::anyhow!("boom");
235        assert_eq!(err.chain_codes().len(), 1);
236        assert_eq!(err.chain_codes()[0], "anyhow");
237    }
238    #[test]
239    fn test_traceable_box_conversion() {
240        fn produce_anyhow() -> anyhow::Result<()> {
241            Err(anyhow::anyhow!("root failure"))
242        }
243
244        fn consume_box() -> Result<(), TraceableBox> {
245            produce_anyhow()?;
246            Ok(())
247        }
248
249        let boxed_err = consume_box().unwrap_err();
250        assert_eq!(boxed_err.chain_codes().len(), 1);
251        assert_eq!(boxed_err.chain_codes()[0], "anyhow");
252        assert_eq!(boxed_err.to_string(), "root failure");
253    }
254
255    #[test]
256    fn test_nested_traceable_box_display() {
257        let root_err = DummyError;
258        let boxed_root: TraceableBox = root_err.into();
259        let anyhow_err = anyhow::Error::new(boxed_root);
260        let boxed_anyhow: TraceableBox = anyhow_err.into();
261
262        let display_str = boxed_anyhow.to_string();
263        assert_eq!(display_str, "DummyError");
264    }
265}