Error handling
Rust
provides
two ways of dealing with errors: Result<T, E>
for recoverable errors and
panic!
for unrecoverable errors. Juniper does not do anything about panicking;
it will bubble up to the surrounding framework and hopefully be dealt with
there.
For recoverable errors, Juniper works well with the built-in Result
type, you
can use the ?
operator or the try!
macro and things will generally just work
as you expect them to:
use juniper::FieldResult;
use std::path::PathBuf;
use std::fs::File;
use std::io::Read;
use std::str;
struct Example {
filename: PathBuf,
}
graphql_object!(Example: () |&self| {
field contents() -> FieldResult<String> {
let mut file = File::open(&self.filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
field foo() -> FieldResult<Option<String>> {
// Some invalid bytes.
let invalid = vec![128, 223];
match str::from_utf8(&invalid) {
Ok(s) => Ok(Some(s.to_string())),
Err(e) => Err(e)?,
}
}
});
FieldResult<T>
is an alias for Result<T, FieldError>
, which is the error
type all fields must return. By using the ?
operator or try!
macro, any type
that implements the Display
trait - which are most of the error types out
there - those errors are automatically converted into FieldError
.
When a field returns an error, the field's result is replaced by null
, an
additional errors
object is created at the top level of the response, and the
execution is resumed. For example, with the previous example and the following
query:
{
example {
contents
foo
}
}
If str::from_utf8
resulted in a std::str::Utf8Error
, the following would be
returned:
Response for nullable field with error
{
"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 }])
]
}
If an error is returned from a non-null field, such as the
example above, the null
value is propagated up to the first nullable parent
field, or the root data
object if there are no nullable fields.
For example, with the following query:
{
example {
contents
}
}
If File::open()
above resulted in std::io::ErrorKind::PermissionDenied
, the
following would be returned:
Response for non-null field with error and no nullable parent
{
"errors": [
"message": "Permission denied (os error 13)",
"locations": [{ "line": 2, "column": 4 }])
]
}
Structured errors
Sometimes it is desirable to return additional structured error information
to clients. This can be accomplished by implementing IntoFieldError
:
use juniper::{FieldError, IntoFieldError};
enum CustomError {
WhateverNotSet,
}
impl IntoFieldError for CustomError {
fn into_field_error(self) -> FieldError {
match self {
CustomError::WhateverNotSet => FieldError::new(
"Whatever does not exist",
graphql_value!({
"type": "NO_WHATEVER"
}),
),
}
}
}
struct Example {
whatever: Option<bool>,
}
graphql_object!(Example: () |&self| {
field whatever() -> Result<bool, CustomError> {
if let Some(value) = self.whatever {
return Ok(value);
}
Err(CustomError::WhateverNotSet)
}
});
The specified structured error information is included in the extensions
key:
{
"errors": [
"message": "Whatever does not exist",
"locations": [{ "line": 2, "column": 4 }]),
"extensions": {
"type": "NO_WHATEVER"
}
]
}