Skip to content

Commit

Permalink
Rework "Error handling" chapter, vol.2
Browse files Browse the repository at this point in the history
  • Loading branch information
tyranron committed Dec 26, 2023
1 parent 6ad43b6 commit 073016f
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 88 deletions.
13 changes: 13 additions & 0 deletions book/src/types/objects/error/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,17 @@ Choosing the right error handling method depends on the requirements of the appl



## Comparison

The [first approach](field.md) (where every error is a [field error][1]) is easier to implement. However, clients won't know what errors may occur and instead will have to infer what happens from the [error message][2]. This is brittle and could change over time due to either clients or server changing. Therefore, extensive integration testing between clients and server is required to maintain the implicit contract between the two.

[Encoding non-critical errors in a GraphQL schema](schema.md) makes the contract between clients and the server explicit. This allows clients to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into a [GraphQL schema][8] requires additional code and up-front definition of non-critical errors.




[GraphQL]: https://graphql.org

[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
[2]: https://spec.graphql.org/October2021/#sel-GAPHRPDCAACCyD57Z
[8]: https://graphql.org/learn/schema
168 changes: 80 additions & 88 deletions book/src/types/objects/error/schema.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
Schema errors
=============

Rust's model of errors can be adapted for GraphQL. Rust's panic is
similar to a `FieldError`--the whole query is aborted and nothing can
be extracted (except for error related information).
[Rust]'s model of errors can be adapted for [GraphQL]. [Rust]'s panic is similar to a [field error][1] - the whole query is aborted and nothing can be extracted (except for error related information).

Not all errors require this strict handling. Recoverable or partial errors can be put
into the GraphQL schema so the client can intelligently handle them.
Not all errors require this strict handling. Recoverable or partial errors can be put into a [GraphQL schema][8], so the client can intelligently handle them.

To implement this approach, all errors must be partitioned into two error classes:
To implement this approach, all errors must be partitioned into two classes:
- _Critical_ errors that cannot be fixed by clients (e.g. a database error).
- _Recoverable_ errors that can be fixed by clients (e.g. invalid input data).

* Critical errors that cannot be fixed by the user (e.g. a database error).
* Recoverable errors that can be fixed by the user (e.g. invalid input data).
Critical errors are returned from resolvers as [field errors][1] (from the [previous chapter](field.md)). Recoverable errors are part of a [GraphQL schema][8] and can be handled gracefully by clients. Similar to [Rust], [GraphQL] allows similar error models with [unions][9] (see ["Unions" chapter](../../unions.md)).

Critical errors are returned from resolvers as `FieldErrors` (from the previous section). Non-critical errors are part of the GraphQL schema and can be handled gracefully by clients. Similar to Rust, GraphQL allows similar error models with unions (see Unions).

### Example Input Validation (simple)

In this example, basic input validation is implemented with GraphQL
types. Strings are used to identify the problematic field name. Errors
for a particular field are also returned as a string. In this example
the string contains a server-side localized error message. However, it is also
possible to return a unique string identifier and have the client present a localized string to the user.
### Example: Simple

In this example, basic input validation is implemented with [GraphQL types][7]. [Strings][5] are used to identify the problematic [field][6] name. Errors for a particular [field][6] are also returned as a [string][5].
```rust
# extern crate juniper;
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
Expand Down Expand Up @@ -54,7 +46,7 @@ pub struct Mutation;

#[graphql_object]
impl Mutation {
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
let mut errors = Vec::new();

if !(10 <= name.len() && name.len() <= 100) {
Expand Down Expand Up @@ -82,14 +74,12 @@ impl Mutation {
# fn main() {}
```

Each function may have a different return type and depending on the input
parameters a new result type is required. For example, adding a user
requires a new result type which contains the variant `Ok(User)`
instead of `Ok(Item)`.
Each function may have a different return type and depending on the input parameters a new result type may be required. For example, adding a `User` would require a new result type containing the variant `Ok(User)`instead of `Ok(Item)`.

The client can send a mutation request and handle the
resulting errors as shown in the following example:
> **NOTE**: In this example the returned [string][5] contains a server-side localized error message. However, it is also
possible to return a unique string identifier and have the client present a localized string to its users.

The client can send a mutation request and handle the resulting errors in the following manner:
```graphql
{
mutation {
Expand All @@ -108,20 +98,14 @@ resulting errors as shown in the following example:
}
```

A useful side effect of this approach is to have partially successful
queries or mutations. If one resolver fails, the results of the
successful resolvers are not discarded.
> **NOTE**: A useful side effect of this approach is to have partially successful queries or mutations. If one resolver fails, the results of the successful resolvers are not discarded.
### Example Input Validation (complex)

Instead of using strings to propagate errors, it is possible to use
GraphQL's type system to describe the errors more precisely.
### Example: Complex

For each fallible input variable a field in a GraphQL object is created. The
field is set if the validation for that particular field fails. You will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than
before. Each resolver function has a custom `ValidationResult` which
contains only fields provided by the function.
Instead of using [strings][5] to propagate errors, it is possible to use [GraphQL type system][7] to describe the errors more precisely.

For each fallible [input argument][4] we create a [field][6] in a [GraphQL object][10]. The [field][6] is set if the validation for that particular [argument][4] fails.
```rust
# extern crate juniper;
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
Expand All @@ -148,7 +132,7 @@ pub struct Mutation;

#[graphql_object]
impl Mutation {
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
let mut error = ValidationError {
name: None,
quantity: None,
Expand All @@ -173,6 +157,9 @@ impl Mutation {
# fn main() {}
```

> **NOTE**: We will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than before. Each resolver function has a custom `ValidationResult` which contains only [fields][6] provided by the function.
So, all the expected errors are handled directly inside the query. Additionally, all non-critical errors are known in advance by both the server and the client:
```graphql
{
mutation {
Expand All @@ -189,24 +176,16 @@ impl Mutation {
}
```

Expected errors are handled directly inside the query. Additionally, all
non-critical errors are known in advance by both the server and the client.

### Example Input Validation (complex with critical error)
### Example: Complex with critical errors

Our examples so far have only included non-critical errors. Providing
errors inside the GraphQL schema still allows you to return unexpected critical
errors when they occur.

In the following example, a theoretical database could fail
and would generate errors. Since it is not common for the database to
fail, the corresponding error is returned as a critical error:
Our examples so far have only included non-critical errors. Providing errors inside a [GraphQL schema][8] still allows us to return unexpected critical errors when they occur.

In the following example, a theoretical database could fail and would generate errors. Since it is not common for a database to fail, the corresponding error is returned as a [critical error][1]:
```rust
# extern crate juniper;
# use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};
#
use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};

#[derive(GraphQLObject)]
pub struct Item {
name: String,
Expand All @@ -232,11 +211,9 @@ pub enum ApiError {
impl<S: ScalarValue> juniper::IntoFieldError<S> for ApiError {
fn into_field_error(self) -> FieldError<S> {
match self {
ApiError::Database => FieldError::new(
Self::Database => FieldError::new(
"Internal database error",
graphql_value!({
"type": "DATABASE"
}),
graphql_value!({"type": "DATABASE"}),
),
}
}
Expand All @@ -246,7 +223,7 @@ pub struct Mutation;

#[graphql_object]
impl Mutation {
fn addItem(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
fn add_item(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
let mut error = ValidationErrorItem {
name: None,
quantity: None,
Expand All @@ -271,41 +248,32 @@ impl Mutation {
# fn main() {}
```

## Additional Material

The [Shopify API](https://shopify.dev/docs/admin-api/graphql/reference)
implements a similar approach. Their API is a good reference to
explore this approach in a real world application.

# Comparison

The first approach discussed above--where every error is a critical error defined by `FieldResult` --is easier to implement. However, the client does not know what errors may occur and must instead infer what happened from the error string. This is brittle and could change over time due to either the client or server changing. Therefore, extensive integration testing between the client and server is required to maintain the implicit contract between the two.

Encoding non-critical errors in the GraphQL schema makes the contract between the client and the server explicit. This allows the client to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into the GraphQL schema requires additional code and up-front definition of non-critical errors.
### Example: Shopify API

The [Shopify API] implements a similar approach. Their API is a good reference to explore this approach in a real world application.

# Non-struct objects

Up until now, we've only looked at mapping structs to GraphQL objects. However,
any Rust type can be mapped into a GraphQL object. In this chapter, we'll look
at enums, but traits will work too - they don't _have_ to be mapped into GraphQL
interfaces.
### Example: Non-struct [objects][10]

Using `Result`-like enums can be a useful way of reporting e.g. validation
errors from a mutation:
Up until now, we've only looked at mapping [structs][20] to [GraphQL objects][10]. However, any [Rust] type can be exposed a [GraphQL object][10].

Using `Result`-like [enums][1] can be a useful way of reporting validation errors from a mutation:
```rust
# extern crate juniper;
# use juniper::{graphql_object, GraphQLObject};
# #[derive(juniper::GraphQLObject)] struct User { name: String }
#
#[derive(GraphQLObject)]
struct User {
name: String,
}

#[derive(GraphQLObject)]
struct ValidationError {
field: String,
message: String,
}

# #[allow(dead_code)]
enum SignUpResult {
Ok(User),
Error(Vec<ValidationError>),
Expand All @@ -314,31 +282,55 @@ enum SignUpResult {
#[graphql_object]
impl SignUpResult {
fn user(&self) -> Option<&User> {
match *self {
SignUpResult::Ok(ref user) => Some(user),
SignUpResult::Error(_) => None,
match self {
Self::Ok(user) => Some(user),
Self::Error(_) => None,
}
}

fn error(&self) -> Option<&Vec<ValidationError>> {
match *self {
SignUpResult::Ok(_) => None,
SignUpResult::Error(ref errors) => Some(errors)
fn error(&self) -> Option<&[ValidationError]> {
match self {
Self::Ok(_) => None,
Self::Error(errs) => Some(errs.as_slice())
}
}
}
#
# fn main() {}
```

Here, we use an enum to decide whether a user's input data was valid or not, and
it could be used as the result of e.g. a sign up mutation.

While this is an example of how you could use something other than a struct to
represent a GraphQL object, it's also an example on how you could implement
error handling for "expected" errors - errors like validation errors. There are
no hard rules on how to represent errors in GraphQL, but there are
[some](https://github.com/facebook/graphql/issues/117#issuecomment-170180628)
[comments](https://github.com/graphql/graphql-js/issues/560#issuecomment-259508214)
from one of the authors of GraphQL on how they intended "hard" field errors to
be used, and how to model expected errors.
Here, we use an [enum][21] to decide whether a client's input data is valid or not, and it could be used as the result of e.g. a `signUp` mutation:
```graphql
{
mutation {
signUp(name: "wrong") {
user {
name
}
error {
field
message
}
}
}
}
```




[GraphQL]: https://graphql.org
[Juniper]: https://docs.rs/juniper
[Rust]: https://www.rust-lang.org
[Shopify API]: https://shopify.dev/docs/admin-api/graphql/reference

[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
[4]: https://spec.graphql.org/October2021#sec-Language.Arguments
[5]: https://spec.graphql.org/October2021#sec-String
[6]: https://spec.graphql.org/October2021#sec-Language.Fields
[7]: https://spec.graphql.org/October2021#sec-Types
[8]: https://graphql.org/learn/schema
[9]: https://spec.graphql.org/October2021#sec-Unions
[10]: https://spec.graphql.org/October2021#sec-Objects
[20]: https://doc.rust-lang.org/reference/items/structs.html
[21]: https://doc.rust-lang.org/reference/items/enumerations.html

0 comments on commit 073016f

Please sign in to comment.