File: mutation_errors.md

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (148 lines) | stat: -rw-r--r-- 4,014 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
---
layout: guide
doc_stub: false
search: true
section: Mutations
title: Mutation errors
desc: Tips for handling and returning errors from mutations
index: 2
---

How can you handle errors inside mutations? Let's explore a couple of options.

## Raising Errors

One way to handle an error is by raising, for example:

```ruby
def resolve(id:, attributes:)
  # Will crash the query if the data is invalid:
  Post.find(id).update!(attributes.to_h)
  # ...
end
```

Or:

```ruby
def resolve(id:, attributes:)
  if post.update(attributes)
    { post: post }
  else
    raise GraphQL::ExecutionError, post.errors.full_messages.join(", ")
  end
end
```

This kind of error handling _does_ express error state (either via `HTTP 500` or by the top-level `"errors"` key), but it doesn't take advantage of GraphQL's type system and can only express one error at a time. It works, but a stronger solution is to treat errors as data.

## Errors as Data

Another way to handle rich error information is to add _error types_ to your schema, for example:

```ruby
class Types::UserError < Types::BaseObject
  description "A user-readable error"

  field :message, String, null: false,
    description: "A description of the error"
  field :path, [String],
    description: "Which input value this error came from"
end
```

Then, add a field to your mutation which uses this error type:

```ruby
class Mutations::UpdatePost < Mutations::BaseMutation
  # ...
  field :errors, [Types::UserError], null: false
end
```

And in the mutation's `resolve` method, be sure to return `errors:` in the hash:

```ruby
def resolve(id:, attributes:)
  post = Post.find(id)
  if post.update(attributes)
    {
      post: post,
      errors: [],
    }
  else
    # Convert Rails model errors into GraphQL-ready error hashes
    user_errors = post.errors.map do |error|
      # This is the GraphQL argument which corresponds to the validation error:
      path = ["attributes", error.attribute.to_s.camelize(:lower)]
      {
        path: path,
        message: error.message,
      }
    end
    {
      post: post,
      errors: user_errors,
    }
  end
end
```

Now that the field returns `errors` in its payload, it supports `errors` as part of the incoming mutations, for example:

```graphql
mutation($postId: ID!, $postAttributes: PostAttributes!) {
  updatePost(id: $postId, attributes: $postAttributes) {
    # This will be present in case of success or failure:
    post {
      title
      comments {
        body
      }
    }
    # In case of failure, there will be errors in this list:
    errors {
      path
      message
    }
  }
}
```

In case of a failure, you might get a response like:

```ruby
{
  "data" => {
    "createPost" => {
      "post" => nil,
      "errors" => [
        { "message" => "Title can't be blank", "path" => ["attributes", "title"] },
        { "message" => "Body can't be blank", "path" => ["attributes", "body"] }
      ]
    }
  }
}
```

Then, client apps can show the error messages to end users, so they might correct the right fields in a form, for example.

## Nullable Mutation Payload Fields

To benefit from "Errors as Data" described above, mutation fields must not have `null: false`. Why?

Well, for _non-null_ fields (which have `null: false`), if they return `nil`, then GraphQL aborts the query and removes those fields from the response altogether.

In mutations, when errors happen, the other fields may return `nil`. So, if those other fields have `null: false`, but they return `nil`, the GraphQL will panic and remove the whole mutation from the response, _including_ the errors!

In order to have the rich error data, even when other fields are `nil`, those fields must have `null: true` (which is the default) so that the type system can be obeyed when errors happen.

Here's an example of a nullable field (good!):

```ruby
class Mutations::UpdatePost < Mutations::BaseMutation
  # Use the default `null: true` to support rich errors:
  field :post, Types::Post
  # ...
end
```