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}