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 {
33    /// Returns this specific layer's string identifier (format: CrateName::EnumName::EnumValue).
34    fn layer_code(&self) -> String;
35
36    /// Recursively interrogates underlying error sources to build the chronological array of layer codes.
37    ///
38    /// The resulting vector is ordered from outermost (most recent) layer to innermost (root cause).
39    fn chain_codes(&self) -> Vec<String>;
40
41    /// Formats the layer code vector into a standardized diagnostic string (e.g., `"Crate1::Enum1::Val1-Crate2::Enum2::Val2"`).
42    fn diagnostic_code(&self) -> String {
43        self.chain_codes().join("-")
44    }
45}
46
47impl TraceableError for anyhow::Error {
48    fn layer_code(&self) -> String {
49        if let Some(boxed) = self.downcast_ref::<TraceableBox>() {
50            boxed.layer_code()
51        } else {
52            "anyhow".to_string()
53        }
54    }
55
56    fn chain_codes(&self) -> Vec<String> {
57        if let Some(boxed) = self.downcast_ref::<TraceableBox>() {
58            // If the anyhow::Error contains a TraceableBox, we traverse into it.
59            // This intentionally bypasses the "anyhow" type-erasing transport layer
60            // to focus on the semantic concrete error chain.
61            boxed.chain_codes()
62        } else {
63            vec!["anyhow".to_string()]
64        }
65    }
66}
67
68/// A concrete, sized encapsulation of a dynamic `TraceableError` trait object.
69///
70/// This wrapper acts as a type-erased boundary. It enables seamless bidirectional `?` operator
71/// compatibility across dynamic crate boundaries, allowing concrete error enums (via `thiserror`)
72/// and untyped conduits (`anyhow`) to nest inside each other without losing causal tracing history.
73///
74/// ## Example
75///
76/// ```rust
77/// use traceable_error::{TraceableError, TraceableBox};
78///
79/// fn produce_anyhow() -> anyhow::Result<()> {
80///     Err(anyhow::anyhow!("root failure"))
81/// }
82///
83/// // Seamlessly converts the anyhow::Error into a TraceableBox trait object via ?
84/// fn consume_box() -> Result<(), TraceableBox> {
85///     produce_anyhow()?;
86///     Ok(())
87/// }
88/// ```
89// Note: TraceableBox intentionally does NOT implement TraceableError.
90// This prevents double-boxing (e.g., wrapping a TraceableBox inside another TraceableBox)
91// at compile time, as it will fail the `E: TraceableError` bound in the `From` implementation.
92#[derive(Debug)]
93pub struct TraceableBox(pub Box<dyn TraceableError + Send + Sync + 'static>);
94
95impl<E: TraceableError + Send + Sync + 'static> From<E> for TraceableBox {
96    fn from(err: E) -> Self {
97        TraceableBox(Box::new(err))
98    }
99}
100
101impl TraceableBox {
102    pub fn layer_code(&self) -> String {
103        self.0.layer_code()
104    }
105
106    pub fn chain_codes(&self) -> Vec<String> {
107        self.0.chain_codes()
108    }
109
110    pub fn diagnostic_code(&self) -> String {
111        self.0.diagnostic_code()
112    }
113}
114
115impl std::fmt::Display for TraceableBox {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        let mut err_str = self.0.to_string();
118        let chain = self.0.chain_codes();
119        let diag_code = self.0.diagnostic_code();
120
121        let mut sub_slice = diag_code.as_str();
122        for (i, layer) in chain.iter().enumerate() {
123            let suffix = format!(" [{}]", sub_slice);
124            if let Some(stripped) = err_str.strip_suffix(&suffix) {
125                err_str = stripped.to_string();
126                break;
127            }
128            if i + 1 < chain.len() {
129                sub_slice = &sub_slice[layer.len() + 1..];
130            }
131        }
132
133        if err_str.is_empty() {
134            write!(f, "[{}]", diag_code)
135        } else {
136            write!(f, "{} [{}]", err_str, diag_code)
137        }
138    }
139}
140
141impl std::error::Error for TraceableBox {}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[derive(Debug)]
148    struct DummyError;
149    impl std::fmt::Display for DummyError {
150        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151            write!(f, "DummyError")
152        }
153    }
154    impl TraceableError for DummyError {
155        fn layer_code(&self) -> String {
156            "DummyError".to_string()
157        }
158        fn chain_codes(&self) -> Vec<String> {
159            vec![self.layer_code()]
160        }
161    }
162
163    #[test]
164    fn test_traceable_error() {
165        let _err = DummyError;
166    }
167
168    #[test]
169    fn test_traceable_box_display_deduplicates_suffix() {
170        #[derive(Debug)]
171        struct InnerError;
172        impl std::fmt::Display for InnerError {
173            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174                write!(f, "root failure")
175            }
176        }
177        impl TraceableError for InnerError {
178            fn layer_code(&self) -> String {
179                "Inner".to_string()
180            }
181            fn chain_codes(&self) -> Vec<String> {
182                vec![self.layer_code()]
183            }
184        }
185
186        #[derive(Debug)]
187        struct OuterError(TraceableBox);
188        impl std::fmt::Display for OuterError {
189            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190                write!(f, "outer wrapper: {}", self.0)
191            }
192        }
193        impl TraceableError for OuterError {
194            fn layer_code(&self) -> String {
195                "Outer".to_string()
196            }
197            fn chain_codes(&self) -> Vec<String> {
198                let mut c = self.0.chain_codes();
199                c.insert(0, self.layer_code());
200                c
201            }
202        }
203
204        let inner_box = TraceableBox::from(InnerError);
205        assert_eq!(inner_box.to_string(), "root failure [Inner]");
206
207        let outer_box = TraceableBox::from(OuterError(inner_box));
208        assert_eq!(outer_box.to_string(), "outer wrapper: root failure [Outer-Inner]");
209    }
210
211    #[test]
212    fn test_anyhow_traceable() {
213        let err = anyhow::anyhow!("boom");
214        assert_eq!(err.chain_codes().len(), 1);
215        assert_eq!(err.chain_codes()[0], "anyhow");
216    }
217    #[test]
218    fn test_traceable_box_conversion() {
219        fn produce_anyhow() -> anyhow::Result<()> {
220            Err(anyhow::anyhow!("root failure"))
221        }
222
223        fn consume_box() -> Result<(), TraceableBox> {
224            produce_anyhow()?;
225            Ok(())
226        }
227
228        let boxed_err = consume_box().unwrap_err();
229        assert_eq!(boxed_err.chain_codes().len(), 1);
230        assert!(boxed_err.to_string().contains("root failure"));
231        assert!(boxed_err.to_string().contains("[anyhow]"));
232    }
233
234    #[test]
235    fn test_nested_traceable_box_display() {
236        let root_err = DummyError;
237        let boxed_root: TraceableBox = root_err.into();
238        let anyhow_err = anyhow::Error::new(boxed_root);
239        let boxed_anyhow: TraceableBox = anyhow_err.into();
240
241        let display_str = boxed_anyhow.to_string();
242        let occurrences = display_str.matches("[DummyError]").count();
243        assert_eq!(
244            occurrences, 1,
245            "Expected '[DummyError]' to appear only once in: {}",
246            display_str
247        );
248    }
249}