Skip to content

Commit 8453310

Browse files
jmpunktLegNeato
andauthored
Documented Alternative Error Handling (#634)
* Added alternative error handling * Fixed book tests and some sentences * Apply suggestions from code review Co-Authored-By: Christian Legnitto <[email protected]> * Fixed book examples Co-authored-by: Christian Legnitto <[email protected]>
1 parent 79c265f commit 8453310

File tree

1 file changed

+286
-1
lines changed

1 file changed

+286
-1
lines changed

docs/book/content/types/objects/error_handling.md

Lines changed: 286 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Error handling
22

3+
Error handling in GraphQL can be done in multiple ways. In the
4+
following two different error handling models are discussed: field
5+
results and GraphQL schema backed errors. Each approach has its
6+
advantages. Choosing the right error handling method depends on the
7+
requirements of the application--investigating both approaches is
8+
beneficial.
9+
10+
## Field Results
11+
312
Rust
413
[provides](https://doc.rust-lang.org/book/second-edition/ch09-00-error-handling.html)
514
two ways of dealing with errors: `Result<T, E>` for recoverable errors and
@@ -115,7 +124,7 @@ following would be returned:
115124
}
116125
```
117126

118-
## Structured errors
127+
### Structured errors
119128

120129
Sometimes it is desirable to return additional structured error information
121130
to clients. This can be accomplished by implementing [`IntoFieldError`](https://docs.rs/juniper/latest/juniper/trait.IntoFieldError.html):
@@ -169,3 +178,279 @@ The specified structured error information is included in the [`extensions`](htt
169178
]
170179
}
171180
```
181+
182+
## Errors Backed by GraphQL's Schema
183+
184+
Rust's model of errors can be adapted for GraphQL. Rust's panic is
185+
similar to a `FieldError`--the whole query is aborted and nothing can
186+
be extracted (except for error related information).
187+
188+
Not all errors require this strict handling. Recoverable or partial errors can be put
189+
into the GraphQL schema so the client can intelligently handle them.
190+
191+
To implement this approach, all errors must be partitioned into two error classes:
192+
193+
* Critical errors that cannot be fixed by the user (e.g. a database error).
194+
* Recoverable errors that can be fixed by the user (e.g. invalid input data).
195+
196+
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).
197+
198+
### Example Input Validation (simple)
199+
200+
In this example, basic input validation is implemented with GraphQL
201+
types. Strings are used to identify the problematic field name. Errors
202+
for a particular field are also returned as a string. In this example
203+
the string contains a server-side localized error message. However, it is also
204+
possible to return a unique string identifier and have the client present a localized string to the user.
205+
206+
```rust
207+
#[derive(juniper::GraphQLObject)]
208+
pub struct Item {
209+
name: String,
210+
quantity: i32,
211+
}
212+
213+
#[derive(juniper::GraphQLObject)]
214+
pub struct ValidationError {
215+
field: String,
216+
message: String,
217+
}
218+
219+
#[derive(juniper::GraphQLObject)]
220+
pub struct ValidationErrors {
221+
errors: Vec<ValidationError>,
222+
}
223+
224+
#[derive(juniper::GraphQLUnion)]
225+
pub enum GraphQLResult {
226+
Ok(Item),
227+
Err(ValidationErrors),
228+
}
229+
230+
pub struct Mutation;
231+
232+
#[juniper::graphql_object]
233+
impl Mutation {
234+
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
235+
let mut errors = Vec::new();
236+
237+
if !(10 <= name.len() && name.len() <= 100) {
238+
errors.push(ValidationError {
239+
field: "name".to_string(),
240+
message: "between 10 and 100".to_string()
241+
});
242+
}
243+
244+
if !(1 <= quantity && quantity <= 10) {
245+
errors.push(ValidationError {
246+
field: "quantity".to_string(),
247+
message: "between 1 and 10".to_string()
248+
});
249+
}
250+
251+
if errors.is_empty() {
252+
GraphQLResult::Ok(Item { name, quantity })
253+
} else {
254+
GraphQLResult::Err(ValidationErrors { errors })
255+
}
256+
}
257+
}
258+
259+
# fn main() {}
260+
```
261+
262+
Each function may have a different return type and depending on the input
263+
parameters a new result type is required. For example, adding a user
264+
requires a new result type which contains the variant `Ok(User)`
265+
instead of `Ok(Item)`.
266+
267+
The client can send a mutation request and handle the
268+
resulting errors as shown in the following example:
269+
270+
```graphql
271+
{
272+
mutation {
273+
addItem(name: "", quantity: 0) {
274+
... on Item {
275+
name
276+
}
277+
... on ValidationErrors {
278+
errors {
279+
field
280+
message
281+
}
282+
}
283+
}
284+
}
285+
}
286+
```
287+
288+
A useful side effect of this approach is to have partially successful
289+
queries or mutations. If one resolver fails, the results of the
290+
successful resolvers are not discarded.
291+
292+
### Example Input Validation (complex)
293+
294+
Instead of using strings to propagate errors, it is possible to use
295+
GraphQL's type system to describe the errors more precisely.
296+
297+
For each fallible input variable a field in a GraphQL object is created. The
298+
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
299+
before. Each resolver function has a custom `ValidationResult` which
300+
contains only fields provided by the function.
301+
302+
```rust
303+
#[derive(juniper::GraphQLObject)]
304+
pub struct Item {
305+
name: String,
306+
quantity: i32,
307+
}
308+
309+
#[derive(juniper::GraphQLObject)]
310+
pub struct ValidationError {
311+
name: Option<String>,
312+
quantity: Option<String>,
313+
}
314+
315+
#[derive(juniper::GraphQLUnion)]
316+
pub enum GraphQLResult {
317+
Ok(Item),
318+
Err(ValidationError),
319+
}
320+
321+
pub struct Mutation;
322+
323+
#[juniper::graphql_object]
324+
impl Mutation {
325+
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
326+
let mut error = ValidationError {
327+
name: None,
328+
quantity: None,
329+
};
330+
331+
if !(10 <= name.len() && name.len() <= 100) {
332+
error.name = Some("between 10 and 100".to_string());
333+
}
334+
335+
if !(1 <= quantity && quantity <= 10) {
336+
error.quantity = Some("between 1 and 10".to_string());
337+
}
338+
339+
if error.name.is_none() && error.quantity.is_none() {
340+
GraphQLResult::Ok(Item { name, quantity })
341+
} else {
342+
GraphQLResult::Err(error)
343+
}
344+
}
345+
}
346+
347+
# fn main() {}
348+
```
349+
350+
```graphql
351+
{
352+
mutation {
353+
addItem {
354+
... on Item {
355+
name
356+
}
357+
... on ValidationErrorsItem {
358+
name
359+
quantity
360+
}
361+
}
362+
}
363+
}
364+
```
365+
366+
Expected errors are handled directly inside the query. Additionally, all
367+
non-critical errors are known in advance by both the server and the client.
368+
369+
### Example Input Validation (complex with critical error)
370+
371+
Our examples so far have only included non-critical errors. Providing
372+
errors inside the GraphQL schema still allows you to return unexpected critical
373+
errors when they occur.
374+
375+
In the following example, a theoretical database could fail
376+
and would generate errors. Since it is not common for the database to
377+
fail, the corresponding error is returned as a critical error:
378+
379+
```rust
380+
# #[macro_use] extern crate juniper;
381+
382+
#[derive(juniper::GraphQLObject)]
383+
pub struct Item {
384+
name: String,
385+
quantity: i32,
386+
}
387+
388+
#[derive(juniper::GraphQLObject)]
389+
pub struct ValidationErrorItem {
390+
name: Option<String>,
391+
quantity: Option<String>,
392+
}
393+
394+
#[derive(juniper::GraphQLUnion)]
395+
pub enum GraphQLResult {
396+
Ok(Item),
397+
Err(ValidationErrorItem),
398+
}
399+
400+
pub enum ApiError {
401+
Database,
402+
}
403+
404+
impl juniper::IntoFieldError for ApiError {
405+
fn into_field_error(self) -> juniper::FieldError {
406+
match self {
407+
ApiError::Database => juniper::FieldError::new(
408+
"Internal database error",
409+
graphql_value!({
410+
"type": "DATABASE"
411+
}),
412+
),
413+
}
414+
}
415+
}
416+
417+
pub struct Mutation;
418+
419+
#[juniper::graphql_object]
420+
impl Mutation {
421+
fn addItem(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
422+
let mut error = ValidationErrorItem {
423+
name: None,
424+
quantity: None,
425+
};
426+
427+
if !(10 <= name.len() && name.len() <= 100) {
428+
error.name = Some("between 10 and 100".to_string());
429+
}
430+
431+
if !(1 <= quantity && quantity <= 10) {
432+
error.quantity = Some("between 1 and 10".to_string());
433+
}
434+
435+
if error.name.is_none() && error.quantity.is_none() {
436+
Ok(GraphQLResult::Ok(Item { name, quantity }))
437+
} else {
438+
Ok(GraphQLResult::Err(error))
439+
}
440+
}
441+
}
442+
443+
# fn main() {}
444+
```
445+
446+
## Additional Material
447+
448+
The [Shopify API](https://shopify.dev/docs/admin-api/graphql/reference)
449+
implements a similar approach. Their API is a good reference to
450+
explore this approach in a real world application.
451+
452+
# Comparison
453+
454+
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.
455+
456+
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.

0 commit comments

Comments
 (0)