Field errors
Rust provides two ways of dealing with errors:
Result<T, E>
for recoverable errors;panic!
for unrecoverable errors.
Juniper does not do anything about panicking, it naturally bubbles up to the surrounding code/framework and can be dealt with there.
For recoverable errors, Juniper works well with the built-in Result
type. You can use the ?
operator and things will work as you expect them to:
extern crate juniper; use std::{fs::File, io::Read, path::PathBuf, str}; use juniper::{graphql_object, FieldResult}; struct Example { filename: PathBuf, } #[graphql_object] impl Example { fn contents(&self) -> FieldResult<String> { let mut file = File::open(&self.filename)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } fn foo() -> FieldResult<Option<String>> { // Some invalid bytes. let invalid = vec![128, 223]; Ok(Some(str::from_utf8(&invalid)?.to_string())) } } fn main() {}
FieldResult<T>
is an alias for Result<T, FieldError>
, which is the error type all fallible fields must return. By using the ?
operator, any type that implements the Display
trait (which most of the error types out there do) can be automatically converted into a FieldError
.
TIP: If a custom conversion into a
FieldError
is needed (to fill upextensions
, for example), theIntoFieldError
trait should be implemented.
NOTE:
FieldError
s are GraphQL field errors and are not visible in a GraphQL schema in any way.
Error payloads, null
, and partial errors
Juniper's error behavior conforms to the GraphQL specification.
When a field returns an error, the field's result is replaced by null
, and an additional errors
object is created at the top level of the response, and the execution is resumed.
Let's run the following query against the previous example:
{
example {
contents
foo
}
}
If str::from_utf8
results in a std::str::Utf8Error
, then the following will be returned:
{
"data": {
"example": {
"contents": "<Contents of the file>",
"foo": null
}
},
"errors": [{
"message": "invalid utf-8 sequence of 2 bytes from index 0",
"locations": [{"line": 2, "column": 4}]
}]
}
Since
Non-Null
type fields cannot be null, field errors are propagated to be handled by the parent field. If the parent field may be null then it resolves to null, otherwise if it is aNon-Null
type, the field error is further propagated to its parent field.
For example, with the following query:
{
example {
contents
}
}
If the File::open()
above results in a std::io::ErrorKind::PermissionDenied
, the following ill be returned:
{
"data": null,
"errors": [{
"message": "Permission denied (os error 13)",
"locations": [{"line": 2, "column": 4}]
}]
}
Additional information
Sometimes it's desirable to return additional structured error information to clients. This can be accomplished by implementing the IntoFieldError
trait:
#[macro_use] extern crate juniper; use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue}; enum CustomError { WhateverNotSet, } impl<S: ScalarValue> IntoFieldError<S> for CustomError { fn into_field_error(self) -> FieldError<S> { match self { Self::WhateverNotSet => FieldError::new( "Whatever does not exist", graphql_value!({ "type": "NO_WHATEVER" }), ), } } } struct Example { whatever: Option<bool>, } #[graphql_object] impl Example { fn whatever(&self) -> Result<bool, CustomError> { if let Some(value) = self.whatever { return Ok(value); } Err(CustomError::WhateverNotSet) } } fn main() {}
And the specified structured error information will be included into the error's extensions
:
{
"errors": [{
"message": "Whatever does not exist",
"locations": [{"line": 2, "column": 4}],
"extensions": {
"type": "NO_WHATEVER"
}
}]
}
NOTE: This pattern is particularly useful when it comes to instrumentation of returned field errors with custom error codes or additional diagnostics (like stack traces or tracing IDs).