Mutation errors

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:

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

Or:

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:

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:

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:

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:

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:

{ "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!):

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