Repository: shepmaster/snafu (rust)

Issue:
Support a backtrace
People like backtraces.

My idea is to support a field called `backtrace`. If it's present, it will be automatically constructed when `From::from` or `fail` is called.

One trick will be to decide how best to have forwards-compatibility. Eventually, the standard library will have such a type and it will accessible via the `Error` trait, but it's not there today. We'd prefer to be able to be able to seamlessly upgrade.

My thought is to have a newtype for the backtrace crate that doesn't expose much in the way of an API. We can then add an inherent method on the `Error` enum with the proposed API for `std::error::Error`.

Things to check / consider:

- What happens when `std::error::Error` gains the method; will that cause conflicts?
- Do we need multiple ways to specify what the backtrace is? (see #10)


Pull request:
Add the ability to capture backtraces
Closes #11

Patch:
diff --git a/.cirrus.yml b/.cirrus.yml
index 06d8ba8..cfb34eb 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -7,8 +7,13 @@ stable_test_task:
   cargo_cache:
     folder: $CARGO_HOME/registry
     fingerprint_script: cat Cargo.toml
-  build_script: cargo build
-  test_script: cargo test
+  build_script:
+    - rustc --version
+    - cargo build
+    - cargo build --manifest-path compatibility-tests/without-backtrace/Cargo.toml
+  test_script:
+    - cargo test
+    - cargo test --manifest-path compatibility-tests/without-backtrace/Cargo.toml
   before_cache_script: rm -rf $CARGO_HOME/registry/index
 
 nightly_test_task:
@@ -21,13 +26,14 @@ nightly_test_task:
     folder: $CARGO_HOME/registry
     fingerprint_script: cat Cargo.toml
   build_script:
-    - cargo build
     - cd compatibility-tests/nightly/
+    - rustc --version
     - cargo build
+    - cargo build --manifest-path ../../Cargo.toml
   test_script:
-    - cargo test
     - cd compatibility-tests/nightly/
     - cargo test
+    - cargo test --manifest-path ../../Cargo.toml
   before_cache_script: rm -rf $CARGO_HOME/registry/index
 
 v1_30_test_task:
@@ -41,6 +47,7 @@ v1_30_test_task:
     fingerprint_script: cat Cargo.toml
   build_script:
     - cd compatibility-tests/v1_30/
+    - rustc --version
     - cargo build
   test_script:
     - cd compatibility-tests/v1_30/
@@ -61,8 +68,14 @@ v1_18_test_task:
     - rustup install 1.18.0
     - rustup default 1.18.0
     - cd compatibility-tests/v1_18/
+    - rustc --version
+    - cargo generate-lockfile
+    # These versions determined by trial and error
+    - cargo update -p backtrace --precise 0.3.0
+    - cargo update -p backtrace-sys --precise 0.1.3
+    - cargo update -p libc --precise 0.2.7
     - cargo build
   test_script:
-    - cd compatibility-tests/v1_18//
+    - cd compatibility-tests/v1_18/
     - cargo test
   before_cache_script: rm -rf $CARGO_HOME/registry/index
diff --git a/Cargo.toml b/Cargo.toml
index d440ce2..5cdd8e6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,10 @@ keywords = ["error", "ergonomic", "library"]
 categories = ["rust-patterns"]
 
 [features]
-default = ["rust_1_30"]
+default = ["rust_1_30", "backtraces"]
+
+# Adds the backtrace type
+backtraces = ["snafu-derive/backtraces", "backtrace"]
 
 # New methods on `Error`; re-export of proc-macro
 rust_1_30 = ["snafu-derive/rust_1_30"]
@@ -29,7 +32,9 @@ exclude = [
   "compatibility-tests/nightly",
   "compatibility-tests/v1_30",
   "compatibility-tests/v1_18",
+  "compatibility-tests/without-backtrace",
 ]
 
 [dependencies]
 snafu-derive = { path = "snafu-derive", version = "0.1.2" }
+backtrace = { version = "0.3.0", optional = true }
diff --git a/snafu-derive/Cargo.toml b/snafu-derive/Cargo.toml
index 7ccb793..03fda9b 100644
--- a/snafu-derive/Cargo.toml
+++ b/snafu-derive/Cargo.toml
@@ -10,6 +10,7 @@ repository = "https://github.com/shepmaster/snafu"
 license = "MIT OR Apache-2.0"
 
 [features]
+backtraces = []
 rust_1_30 = []
 unstable_display_attribute = []
 
diff --git a/snafu-derive/src/lib.rs b/snafu-derive/src/lib.rs
index f38e165..639d78e 100644
--- a/snafu-derive/src/lib.rs
+++ b/snafu-derive/src/lib.rs
@@ -27,6 +27,7 @@ struct EnumInfo {
 struct VariantInfo {
     name: syn::Ident,
     source_field: Option<Field>,
+    backtrace_field: Option<Field>,
     user_fields: Vec<Field>,
     display_format: Option<DisplayFormat>,
 }
@@ -82,6 +83,7 @@ fn parse_snafu_information(ty: syn::DeriveInput) -> EnumInfo {
 
         let mut user_fields = Vec::new();
         let mut source_fields = Vec::new();
+        let mut backtrace_fields = Vec::new();
 
         for field in fields {
             let name = field.ident.expect("Must have a named field");
@@ -89,6 +91,8 @@ fn parse_snafu_information(ty: syn::DeriveInput) -> EnumInfo {
 
             if field.name == "source" {
                 source_fields.push(field);
+            } else if field.name == "backtrace" {
+                backtrace_fields.push(field);
             } else {
                 user_fields.push(field);
             }
@@ -97,7 +101,10 @@ fn parse_snafu_information(ty: syn::DeriveInput) -> EnumInfo {
         let source_field = source_fields.pop();
         // Report a warning if there are multiple?
 
-        VariantInfo { name, source_field, user_fields, display_format }
+        let backtrace_field = backtrace_fields.pop();
+        // Report a warning if there are multiple?
+
+        VariantInfo { name, source_field, backtrace_field, user_fields, display_format }
     }).collect();
 
     EnumInfo { name, variants }
@@ -171,7 +178,7 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
     let enum_name = enum_info.name;
 
     let generated_variant_support = enum_info.variants.iter().map(|variant| {
-        let VariantInfo { name: ref variant_name, ref source_field, ref user_fields, .. } = *variant;
+        let VariantInfo { name: ref variant_name, ref source_field, ref backtrace_field, ref user_fields, .. } = *variant;
 
         let generic_names: Vec<_> = (0..)
             .map(|i| Ident::new(&format!("T{}", i), Span::call_site()))
@@ -200,6 +207,13 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
             }
         };
 
+        let backtrace_field = match *backtrace_field {
+            Some(_) => {
+                quote! { backtrace: std::default::Default::default(), }
+            }
+            None => quote! {},
+        };
+
         let where_clauses: Vec<_> = generic_names.iter().zip(user_fields).map(|(gen_ty, f)| {
             let Field { ref ty, .. } = *f;
             quote! { #gen_ty: std::convert::Into<#ty> }
@@ -216,6 +230,7 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
                     fn fail<T>(self) -> std::result::Result<T, #enum_name> {
                         let Self { #(#names),* } = self;
                         let error = #enum_name::#variant_name {
+                            #backtrace_field
                             #( #names: std::convert::Into::into(#names2) ),*
                         };
                         std::result::Result::Err(error)
@@ -247,6 +262,7 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
                         fn from(other: #other_ty) -> Self {
                             #enum_name::#variant_name {
                                 #source_name: other.error,
+                                #backtrace_field
                                 #(#user_fields),*
                             }
                         }
@@ -266,7 +282,7 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
     });
 
     let variants = enum_info.variants.iter().map(|variant| {
-        let VariantInfo { name: ref variant_name, ref user_fields, ref source_field, ref display_format, .. } = *variant;
+        let VariantInfo { name: ref variant_name, ref user_fields, ref source_field, ref backtrace_field, ref display_format, .. } = *variant;
 
         let format = match *display_format {
             Some(DisplayFormat::Stringified(ref fmt)) => {
@@ -279,7 +295,7 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
         };
 
 
-        let field_names = user_fields.iter().chain(source_field).map(|f| &f.name);
+        let field_names = user_fields.iter().chain(source_field).chain(backtrace_field).map(|f| &f.name);
         let field_names = quote! { #(ref #field_names),* };
 
         quote! {
@@ -364,9 +380,46 @@ fn generate_snafu(enum_info: EnumInfo) -> proc_macro2::TokenStream {
         }
     };
 
+    let variants = enum_info.variants.iter().map(|variant| {
+        let VariantInfo { name: ref variant_name, ref backtrace_field, .. } = *variant;
+
+        match *backtrace_field {
+            Some(ref backtrace_field) => {
+                let Field { name: ref field_name, .. } = *backtrace_field;
+                quote! {
+                    #enum_name::#variant_name { ref #field_name, .. } => { Some(#field_name) }
+                }
+            }
+            None => {
+                quote! {
+                    #enum_name::#variant_name { .. } => { None }
+                }
+            }
+        }
+    });
+
+    let backtrace_fn = if cfg!(feature = "backtraces") {
+        quote! {
+            fn backtrace(&self) -> Option<&snafu::Backtrace> {
+                match *self {
+                    #(#variants),*
+                }
+            }
+        }
+    } else {
+        quote! { }
+    };
+
+    let error_compat_impl = quote ! {
+        impl snafu::ErrorCompat for #enum_name {
+            #backtrace_fn
+        }
+    };
+
     quote! {
         #(#generated_variant_support)*
         #display_impl
         #error_impl
+        #error_compat_impl
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index ce5459e..4c4056f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,7 +21,7 @@
 //! method to provide ergonomic error handling.
 //!
 //! ```rust
-//! use snafu::{Snafu, ResultExt};
+//! use snafu::{Snafu, ResultExt, Backtrace, ErrorCompat};
 //! use std::{fs, path::{Path, PathBuf}};
 //!
 //! #[derive(Debug, Snafu)]
@@ -31,12 +31,12 @@
 //!     #[snafu_display("Could not save config to {}: {}", "filename.display()", "source")]
 //!     SaveConfig { filename: PathBuf, source: std::io::Error },
 //!     #[snafu_display("The user id {} is invalid", "user_id")]
-//!     UserIdInvalid { user_id: i32 },
+//!     UserIdInvalid { user_id: i32, backtrace: Backtrace },
 //! }
 //!
 //! type Result<T, E = Error> = std::result::Result<T, E>;
 //!
-//! fn log_in_user<P>(config_root: P, user_id: i32) -> Result<()>
+//! fn log_in_user<P>(config_root: P, user_id: i32) -> Result<bool>
 //! where
 //!     P: AsRef<Path>,
 //! {
@@ -51,7 +51,22 @@
 //!         UserIdInvalid { user_id }.fail()?;
 //!     }
 //!
-//!     Ok(())
+//!     Ok(true)
+//! }
+//!
+//! # const CONFIG_DIRECTORY: &str = "/does/not/exist";
+//! # const USER_ID: i32 = 0;
+//! fn log_in() {
+//!     match log_in_user(CONFIG_DIRECTORY, USER_ID) {
+//!         Ok(true) => println!("Logged in!"),
+//!         Ok(false) => println!("Not logged in!"),
+//!         Err(e) => {
+//!             eprintln!("An error occurred: {}", e);
+//!             if let Some(backtrace) = ErrorCompat::backtrace(&e) {
+//!                 println!("{}", backtrace);
+//!             }
+//!         }
+//!     }
 //! }
 //! ```
 //!
@@ -92,9 +107,10 @@
 //!
 //! 1. One struct is created for each enum variant.
 //! 1. The name of the struct is the same as the enum variant's name.
-//! 1. The `source` field has been removed; the library will
-//! automatically handle this for you.
-//! 1. Each field's type has been replaced with a generic type.
+//! 1. The `source` and `backtrace` fields have been removed; the
+//!    library will automatically handle this for you.
+//! 1. Each remaining field's type has been replaced with a generic
+//!    type.
 //!
 //! If the original variant had a `source` field, its context selector
 //! will have an implementation of [`From`](std::convert::From) for a
@@ -118,6 +134,10 @@
 //! }
 //! ```
 //!
+//! If the original variant had a `backtrace` field, the backtrace
+//! will be automatically constructed when either `From` or `fail` are
+//! called.
+//!
 //! ### Attributes
 //!
 //! #### Controlling `Display`
@@ -168,6 +188,17 @@
 //!
 //! - Adds support for the `snafu::display` attribute.
 //!
+//! ## Other feature flags
+//!
+//! ### `backtraces`
+//!
+//! When enabled, you can use the [`Backtrace`](Backtrace) type in
+//! your enum variant. If you never use backtraces, you can omit this
+//! feature to speed up compilation a small amount.
+
+#[cfg(feature = "backtraces")]
+extern crate backtrace;
+
 #[cfg(feature = "rust_1_30")]
 extern crate snafu_derive;
 #[cfg(feature = "rust_1_30")]
@@ -337,3 +368,121 @@ impl<T, E> ResultExt<T, E> for std::result::Result<T, E> {
         })
     }
 }
+
+/// Backports changes to the [`Error`](std::error::Error) trait to
+/// versions of Rust lacking them.
+///
+/// It is recommended to always call these methods explicitly so that
+/// it is easy to replace usages of this trait when you start
+/// supporting a newer version of Rust.
+///
+/// ```
+/// # use snafu::{Snafu, ErrorCompat};
+/// # #[derive(Debug, Snafu)] enum Example {};
+/// # fn example(error: Example) {
+/// ErrorCompat::backtrace(&error); // Recommended
+/// error.backtrace();              // Discouraged
+/// # }
+/// ```
+pub trait ErrorCompat {
+    /// Returns a [`Backtrace`](Backtrace) that may be printed.
+    #[cfg(feature = "backtraces")]
+    fn backtrace(&self) -> Option<&Backtrace> {
+        None
+    }
+}
+
+#[cfg(feature = "backtraces")]
+pub use backtrace_shim::*;
+
+#[cfg(feature = "backtraces")]
+mod backtrace_shim {
+    use backtrace;
+    use std::{fmt, path};
+
+    /// A backtrace starting from the beginning of the thread.
+    #[derive(Debug)]
+    pub struct Backtrace(backtrace::Backtrace);
+
+    impl Backtrace {
+        /// Creates the backtrace.
+        // Inlining in an attempt to remove this function from the backtrace
+        #[inline(always)]
+        pub fn new() -> Self {
+            Backtrace(backtrace::Backtrace::new())
+        }
+    }
+
+    impl Default for Backtrace {
+        // Inlining in an attempt to remove this function from the backtrace
+        #[inline(always)]
+        fn default() -> Self {
+            Self::new()
+        }
+    }
+
+    impl fmt::Display for Backtrace {
+        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+            let frames = self.0.frames();
+            let width = (frames.len() as f32).log10().floor() as usize + 1;
+
+            for (index, frame) in frames.iter().enumerate() {
+                let mut symbols = frame.symbols().iter().map(SymbolDisplay);
+
+                if let Some(symbol) = symbols.next() {
+                    writeln!(f, "{index:width$} {name}", index = index, width = width, name = symbol.name())?;
+                    if let Some(location) = symbol.location() {
+                        writeln!(f, "{index:width$} {location}", index = "", width = width, location = location)?;
+                    }
+
+                    for symbol in symbols {
+                        writeln!(f, "{index:width$} {name}", index = "", width = width, name = symbol.name())?;
+                        if let Some(location) = symbol.location() {
+                            writeln!(f, "{index:width$} {location}", index = "", width = width, location = location)?;
+                        }
+                    }
+                }
+            }
+
+            Ok(())
+        }
+    }
+
+    struct SymbolDisplay<'a>(&'a backtrace::BacktraceSymbol);
+
+    impl<'a> SymbolDisplay<'a> {
+        fn name(&self) -> SymbolNameDisplay<'a> {
+            SymbolNameDisplay(self.0)
+        }
+
+        fn location(&self) -> Option<SymbolLocationDisplay<'a>> {
+            self.0.filename().map(|f| SymbolLocationDisplay(self.0, f))
+        }
+    }
+
+    struct SymbolNameDisplay<'a>(&'a backtrace::BacktraceSymbol);
+
+    impl<'a> fmt::Display for SymbolNameDisplay<'a> {
+        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+            match self.0.name() {
+                Some(n) => write!(f, "{}", n)?,
+                None => write!(f, "<unknown>")?,
+            }
+
+            Ok(())
+        }
+    }
+
+    struct SymbolLocationDisplay<'a>(&'a backtrace::BacktraceSymbol, &'a path::Path);
+
+    impl<'a> fmt::Display for SymbolLocationDisplay<'a> {
+        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+            write!(f, "{}", self.1.display())?;
+            if let Some(l) = self.0.lineno() {
+                write!(f, ":{}", l)?;
+            }
+
+            Ok(())
+        }
+    }
+}


Test patch:
diff --git a/compatibility-tests/v1_18/Cargo.toml b/compatibility-tests/v1_18/Cargo.toml
index e11b3da..64949da 100644
--- a/compatibility-tests/v1_18/Cargo.toml
+++ b/compatibility-tests/v1_18/Cargo.toml
@@ -4,5 +4,5 @@ version = "0.1.0"
 authors = ["Jake Goulding <jake.goulding@gmail.com>"]
 
 [dependencies]
-snafu = { path = "../..", default-features = false }
+snafu = { path = "../..", default-features = false, features = ["backtraces"] }
 snafu-derive = { path = "../../snafu-derive" }
diff --git a/compatibility-tests/v1_18/tests/v1_18.rs b/compatibility-tests/v1_18/tests/v1_18.rs
index d671061..8219d5c 100644
--- a/compatibility-tests/v1_18/tests/v1_18.rs
+++ b/compatibility-tests/v1_18/tests/v1_18.rs
@@ -2,7 +2,7 @@ extern crate snafu;
 #[macro_use]
 extern crate snafu_derive;
 
-use snafu::ResultExt;
+use snafu::{ResultExt, Backtrace, ErrorCompat};
 use std::io;
 use std::path::{Path, PathBuf};
 
@@ -13,7 +13,7 @@ enum Error {
     #[snafu_display = r#"("Could not open config file at {}", source)"#]
     SaveConfig { source: io::Error },
     #[snafu_display = r#"("User ID {} is invalid", user_id)"#]
-    InvalidUser { user_id: i32 },
+    InvalidUser { user_id: i32, backtrace: Backtrace },
     #[snafu_display = r#"("No user available")"#]
     MissingUser,
 }
@@ -71,5 +71,6 @@ where
 fn implements_error() {
     fn check<T: std::error::Error>() {}
     check::<Error>();
-    example("/some/directory/that/does/not/exist", None).unwrap_err();
+    let e = example("/some/directory/that/does/not/exist", None).unwrap_err();
+    ErrorCompat::backtrace(&e);
 }
diff --git a/compatibility-tests/v1_30/Cargo.toml b/compatibility-tests/v1_30/Cargo.toml
index 827a81c..06d219a 100644
--- a/compatibility-tests/v1_30/Cargo.toml
+++ b/compatibility-tests/v1_30/Cargo.toml
@@ -4,4 +4,4 @@ version = "0.1.0"
 authors = ["Jake Goulding <jake.goulding@gmail.com>"]
 
 [dependencies]
-snafu = { path = "../..", default-features = false, features = ["rust_1_30"] }
+snafu = { path = "../..", default-features = false, features = ["rust_1_30", "backtraces"] }
diff --git a/compatibility-tests/v1_30/tests/v1_30.rs b/compatibility-tests/v1_30/tests/v1_30.rs
index d027cb4..8a455c9 100644
--- a/compatibility-tests/v1_30/tests/v1_30.rs
+++ b/compatibility-tests/v1_30/tests/v1_30.rs
@@ -1,6 +1,6 @@
 extern crate snafu;
 
-use snafu::{Snafu, ResultExt};
+use snafu::{Snafu, ResultExt, Backtrace, ErrorCompat};
 use std::{fs, io, path::{Path, PathBuf}};
 
 #[derive(Debug, Snafu)]
@@ -10,7 +10,7 @@ enum Error {
     #[snafu_display("Could not open config file at {}", "source")]
     SaveConfig { source: io::Error },
     #[snafu_display("User ID {} is invalid", "user_id")]
-    InvalidUser { user_id: i32 },
+    InvalidUser { user_id: i32, backtrace: Backtrace },
     #[snafu_display("No user available")]
     MissingUser,
 }
@@ -40,5 +40,6 @@ fn example(root: impl AsRef<Path>, user_id: Option<i32>) -> Result<()> {
 fn implements_error() {
     fn check<T: std::error::Error>() {}
     check::<Error>();
-    example("/some/directory/that/does/not/exist", None).unwrap_err();
+    let e = example("/some/directory/that/does/not/exist", None).unwrap_err();
+    ErrorCompat::backtrace(&e);
 }
diff --git a/compatibility-tests/without-backtrace/.gitignore b/compatibility-tests/without-backtrace/.gitignore
new file mode 100644
index 0000000..6936990
--- /dev/null
+++ b/compatibility-tests/without-backtrace/.gitignore
@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+Cargo.lock
diff --git a/compatibility-tests/without-backtrace/Cargo.toml b/compatibility-tests/without-backtrace/Cargo.toml
new file mode 100644
index 0000000..a41d60d
--- /dev/null
+++ b/compatibility-tests/without-backtrace/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "without-backtrace"
+version = "0.1.0"
+authors = ["Jake Goulding <jake.goulding@gmail.com>"]
+edition = "2018"
+
+[dependencies]
+snafu = { path = "../..", default-features = false, features = ["rust_1_30"] }
diff --git a/compatibility-tests/without-backtrace/src/lib.rs b/compatibility-tests/without-backtrace/src/lib.rs
new file mode 100644
index 0000000..e69de29
diff --git a/compatibility-tests/without-backtrace/tests/without_backtrace.rs b/compatibility-tests/without-backtrace/tests/without_backtrace.rs
new file mode 100644
index 0000000..06492de
--- /dev/null
+++ b/compatibility-tests/without-backtrace/tests/without_backtrace.rs
@@ -0,0 +1,15 @@
+extern crate snafu;
+
+use snafu::Snafu;
+
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu_display("User ID {} is invalid", "user_id")]
+    InvalidUser { user_id: i32 },
+}
+
+#[test]
+fn implements_error() {
+    fn check<T: std::error::Error>() {}
+    check::<Error>();
+}
diff --git a/tests/backtrace.rs b/tests/backtrace.rs
new file mode 100644
index 0000000..3397898
--- /dev/null
+++ b/tests/backtrace.rs
@@ -0,0 +1,58 @@
+extern crate snafu;
+
+use snafu::{Backtrace, Snafu, ResultExt, ErrorCompat};
+
+type AnotherError = Box<std::error::Error>;
+
+#[derive(Debug, Snafu)]
+enum Error {
+    #[snafu_display("Invalid user {}:\n{}", "user_id", "backtrace")]
+    InvalidUser { user_id: i32, backtrace: Backtrace },
+    WithSource { source: AnotherError, backtrace: Backtrace },
+    WithSourceAndOtherInfo { user_id: i32, source: AnotherError, backtrace: Backtrace },
+}
+
+type Result<T, E = Error> = std::result::Result<T, E>;
+
+fn check_less_than(user_id: i32) -> Result<()> {
+    if user_id < 42 { InvalidUser { user_id }.fail()?; }
+    Ok(())
+}
+
+fn check_greater_than(user_id: i32) -> Result<()> {
+    if user_id > 42 { InvalidUser { user_id }.fail()?; }
+    Ok(())
+}
+
+fn example(user_id: i32) -> Result<()> {
+    check_less_than(user_id)?;
+    check_greater_than(user_id)?;
+
+    Ok(())
+}
+
+#[test]
+fn has_a_backtrace() {
+    let e = example(0).unwrap_err();
+    let text = ErrorCompat::backtrace(&e).map(ToString::to_string).unwrap_or_default();
+    assert!(text.contains("check_less_than"));
+}
+
+#[test]
+fn display_can_access_backtrace() {
+    let e = example(0).unwrap_err();
+    let text = e.to_string();
+    assert!(text.contains("check_less_than"));
+}
+
+fn trigger() -> Result<(), AnotherError> { Err("boom".into()) }
+
+#[test]
+fn errors_with_sources_can_have_backtraces() {
+    let _e: Error = trigger().eager_context(WithSource).unwrap_err();
+}
+
+#[test]
+fn errors_with_sources_and_other_info_can_have_backtraces() {
+    let _e: Error = trigger().eager_context(WithSourceAndOtherInfo { user_id: 42 }).unwrap_err();
+}