Repository: graphql-rust/juniper (language: rust)

Issue:
`IntoFieldResult` for `anyhow::Error`
**Is your feature request related to a problem? Please describe.**
[anyhow](https://github.com/dtolnay/anyhow) is a dominant error handling library for rust. Currently, there is no convenient way to use anyhow `Err`s in juniper code. For example,
```
struct Mutation;

#[juniper::graphql_object]
impl Mutation {
  async fn Foo(line_number: i32) -> FieldResult<bool> {
    anyhow::ensure!(line_number >= 0);
    Ok(true)
  }
}
```
doesn't work.

[IntoFieldResult](https://docs.rs/juniper/0.15.7/juniper/trait.IntoFieldResult.html) offers a way of translating custom error types into `FieldResult`s, but unfortunately implementing `IntoFieldResult` for `anyhow::Error` is not possible due to https://doc.rust-lang.org/error-index.html#E0117.

Side note: why is there `IntoFieldResult` instead of simply `Into<FieldResult>`?

**Describe the solution you'd like**
Not 100% clear what the exact solution ought to be, but I think just implementing `IntoFieldResult` for `anyhow::Error` would do the trick.

**Describe alternatives you've considered**
n/a

**Additional context**
n/a



Discussion / hints:
My hacky workaround is to just use a macro:
```rust
macro_rules! juniper_ensure {
  // TODO why doesn't recursion work?
  ($cond:expr $(,)?) => {
    if !$cond {
      return Err(juniper::FieldError::new(
        concat!("Condition failed: `", stringify!($cond), "`"),
        juniper::Value::null(),
      ));
    }
  };
  ($cond:expr, $msg:literal $(,)?) => {
    if !$cond {
      return Err(juniper::FieldError::new($msg, juniper::Value::null()));
    }
  };
}
```

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fb649b667..27e879992 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -99,6 +99,8 @@ jobs:
       matrix:
         include:
           - { feature: <none>, crate: juniper }
+          - { feature: anyhow, crate: juniper }
+          - { feature: "anyhow,backtrace", crate: juniper }
           - { feature: bigdecimal, crate: juniper }
           - { feature: bson, crate: juniper }
           - { feature: chrono, crate: juniper }
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 26939bad8..dc18fb8cb 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -69,6 +69,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 - `js` [Cargo feature] enabling `js-sys` and `wasm-bindgen` support for `wasm32-unknown-unknown` target. ([#1118], [#1147])
 - `LookAheadMethods::applies_for()` method. ([#1138], [#1145])
 - `LookAheadMethods::field_original_name()` and `LookAheadMethods::field_alias()` methods. ([#1199])
+- [`anyhow` crate] integration behind `anyhow` and `backtrace` [Cargo feature]s. ([#1215], [#988])
 
 ### Changed
 
@@ -98,6 +99,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 [#979]: /../../pull/979
 [#985]: /../../pull/985
 [#987]: /../../pull/987
+[#988]: /../../issues/988
 [#996]: /../../pull/996
 [#1000]: /../../issues/1000
 [#1001]: /../../pull/1001
@@ -137,6 +139,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 [#1207]: /../../pull/1207
 [#1208]: /../../pull/1208
 [#1209]: /../../pull/1209
+[#1215]: /../../pull/1215
 [ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
 [CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
 
@@ -150,6 +153,7 @@ See [old CHANGELOG](/../../blob/juniper-v0.15.9/juniper/CHANGELOG.md).
 
 
 
+[`anyhow` crate]: https://docs.rs/anyhow
 [`bigdecimal` crate]: https://docs.rs/bigdecimal
 [`bson` crate]: https://docs.rs/bson
 [`chrono` crate]: https://docs.rs/chrono
diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml
index 7c474d25d..3e87e1081 100644
--- a/juniper/Cargo.toml
+++ b/juniper/Cargo.toml
@@ -31,6 +31,8 @@ default = [
     "url",
     "uuid",
 ]
+anyhow = ["dep:anyhow"]
+backtrace = ["anyhow?/backtrace"]
 bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "dep:ryu"]
 bson = ["dep:bson"]
 chrono = ["dep:chrono"]
@@ -46,7 +48,7 @@ url = ["dep:url"]
 uuid = ["dep:uuid"]
 
 [dependencies]
-anyhow = { version = "1.0.47", default-features = false, optional = true }
+anyhow = { version = "1.0.47", optional = true }
 async-trait = "0.1.39"
 bigdecimal = { version = "0.4", optional = true }
 bson = { version = "2.4", features = ["chrono-0_4"], optional = true }
@@ -81,6 +83,7 @@ bencher = "0.1.2"
 chrono = { version = "0.4.30", features = ["alloc"], default-features = false }
 pretty_assertions = "1.0.0"
 serde_json = "1.0.18"
+serial_test = "2.0"
 tokio = { version = "1.0", features = ["macros", "time", "rt-multi-thread"] }
 
 [[bench]]
diff --git a/juniper/src/integrations/anyhow.rs b/juniper/src/integrations/anyhow.rs
new file mode 100644
index 000000000..11217f42b
--- /dev/null
+++ b/juniper/src/integrations/anyhow.rs
@@ -0,0 +1,131 @@
+//! GraphQL support for [`anyhow::Error`].
+//!
+//! # Example
+//!
+//! ```rust
+//! # use std::backtrace::Backtrace;
+//! use anyhow::anyhow;
+//! # use juniper::graphql_object;
+//!
+//! struct Root;
+//!
+//! #[graphql_object]
+//! impl Root {
+//!     fn err() -> anyhow::Result<i32> {
+//!         Err(anyhow!("errored!"))
+//!     }
+//! }
+//! ```
+//!
+//! # Backtrace
+//!
+//! Backtrace is supported in the same way as [`anyhow`] crate does:
+//! > If using the nightly channel, or stable with `features = ["backtrace"]`, a backtrace is
+//! > captured and printed with the error if the underlying error type does not already provide its
+//! > own. In order to see backtraces, they must be enabled through the environment variables
+//! > described in [`std::backtrace`]:
+//! > - If you want panics and errors to both have backtraces, set `RUST_BACKTRACE=1`;
+//! > - If you want only errors to have backtraces, set `RUST_LIB_BACKTRACE=1`;
+//! > - If you want only panics to have backtraces, set `RUST_BACKTRACE=1` and
+//! >   `RUST_LIB_BACKTRACE=0`.
+
+use crate::{FieldError, IntoFieldError, ScalarValue, Value};
+
+impl<S: ScalarValue> IntoFieldError<S> for anyhow::Error {
+    fn into_field_error(self) -> FieldError<S> {
+        #[cfg(any(nightly, feature = "backtrace"))]
+        let extensions = {
+            let backtrace = self.backtrace().to_string();
+            if backtrace == "disabled backtrace" {
+                Value::Null
+            } else {
+                let mut obj = crate::value::Object::with_capacity(1);
+                _ = obj.add_field(
+                    "backtrace",
+                    Value::List(
+                        backtrace
+                            .split('\n')
+                            .map(|line| Value::Scalar(line.to_owned().into()))
+                            .collect(),
+                    ),
+                );
+                Value::Object(obj)
+            }
+        };
+        #[cfg(not(any(nightly, feature = "backtrace")))]
+        let extensions = Value::Null;
+
+        FieldError::new(self, extensions)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::env;
+
+    use anyhow::anyhow;
+    use serial_test::serial;
+
+    use crate::{
+        execute, graphql_object, graphql_value, graphql_vars, parser::SourcePosition,
+        EmptyMutation, EmptySubscription, RootNode,
+    };
+
+    #[tokio::test]
+    #[serial]
+    async fn simple() {
+        struct Root;
+
+        #[graphql_object]
+        impl Root {
+            fn err() -> anyhow::Result<i32> {
+                Err(anyhow!("errored!"))
+            }
+        }
+
+        let prev_env = env::var("RUST_BACKTRACE").ok();
+        env::set_var("RUST_BACKTRACE", "1");
+
+        const DOC: &str = r#"{
+            err
+        }"#;
+
+        let schema = RootNode::new(
+            Root,
+            EmptyMutation::<()>::new(),
+            EmptySubscription::<()>::new(),
+        );
+
+        let res = execute(DOC, None, &schema, &graphql_vars! {}, &()).await;
+
+        assert!(res.is_ok(), "failed: {:?}", res.unwrap_err());
+
+        let (val, errs) = res.unwrap();
+
+        assert_eq!(val, graphql_value!(null));
+        assert_eq!(errs.len(), 1, "too many errors: {errs:?}");
+
+        let err = errs.first().unwrap();
+
+        assert_eq!(*err.location(), SourcePosition::new(14, 1, 12));
+        assert_eq!(err.path(), &["err"]);
+
+        let err = err.error();
+
+        assert_eq!(err.message(), "errored!");
+        #[cfg(not(any(nightly, feature = "backtrace")))]
+        assert_eq!(err.extensions(), &graphql_value!(null));
+        #[cfg(any(nightly, feature = "backtrace"))]
+        assert_eq!(
+            err.extensions()
+                .as_object_value()
+                .map(|ext| ext.contains_field("backtrace")),
+            Some(true),
+            "no `backtrace` in extensions: {err:?}",
+        );
+
+        if let Some(val) = prev_env {
+            env::set_var("RUST_BACKTRACE", val);
+        }
+    }
+}
diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs
index 05f988cb4..0d88bdf2c 100644
--- a/juniper/src/integrations/mod.rs
+++ b/juniper/src/integrations/mod.rs
@@ -1,5 +1,7 @@
 //! Provides GraphQLType implementations for some external types
 
+#[cfg(feature = "anyhow")]
+pub mod anyhow;
 #[cfg(feature = "bigdecimal")]
 pub mod bigdecimal;
 #[cfg(feature = "bson")]


Test patch:
diff --git a/tests/integration/tests/codegen_object_attr.rs b/tests/integration/tests/codegen_object_attr.rs
index 1d1436e0a..44cbc5d25 100644
--- a/tests/integration/tests/codegen_object_attr.rs
+++ b/tests/integration/tests/codegen_object_attr.rs
@@ -332,7 +332,7 @@ mod fallible_method {
 
     impl<S: ScalarValue> IntoFieldError<S> for CustomError {
         fn into_field_error(self) -> FieldError<S> {
-            juniper::FieldError::new("Whatever", graphql_value!({"code": "some"}))
+            FieldError::new("Whatever", graphql_value!({"code": "some"}))
         }
     }