DEV Community

Masa
Masa

Posted on

GraphQL error handling with graphql-ruby

If you want to develop a stable, high availablility application, error-handling is something you have to pay great attention to.
GraphQL is a fairly new protocol compared to RESTful and it should be dealt with completely different approaches given its unique way to communicate with clients.
As I started developing some application with Ruby on Rails and graphql-ruby, I realized that you don't see many articles on this topic yet, so I decided to share the way I do error-handling with GraphQL and Ruby on Rails.

First I'll quickly explain GraphQL's specification and practices, then go on to the main topic which is how I implement error-handling with graphql-ruby.

table of contents

  • GraphQL's specification and practices
  • The variety of errors you get with GraphQL
  • The implementation with graphql-ruby
  • safety net with rescue_from method
  • handling multiple errors with add_error method

GraphQL's specification and practices

practices on response's format in GraphQL

One of the GraphQL's practices is that every response is returned with http status 200 and occurred errors are included errors key in the response body.
That is because you can include multiple queries in a request in GraphQL.

Since GraphQL allows for multiple operations to be sent in the same request, it's well possible that a request only partially fails and returns actual data and errors.

https://www.graph.cool/docs/faq/api-eep0ugh1wa/#how-does-error-handling-work-with-graphcool

This is a practice not specification but related tools like Apollo and graphql-ruby follow this practice and that makes us follow it too.

GraphQL's specification about response's format

Then how do you express errors in GraphQL?
Let's take a look at the GraphQL's specification here.

https://facebook.github.io/graphql/June2018/#sec-Errors

According to the GraphQL's specification, response's format is Hash with keys named data and errors.
data is the key that has the actual data and errors is the key that has errors occurred during the execution.

{
  "errors": [
    {
      "message": "hogehoge",
      "extensions": {
        "bar": "bar"
      }
    }
  ],
  "data": {
    "user": {
      "name": "Jon"
    }
  }
}

By the way, the format of a hash inside errors is specified in specification as it has 3 keys named message, location and path.
If you wanna have it have another key, create a key named extensions and put your key in it like shown below.

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

You want to add your own key this way because it might potentially conflicts with keys added in the future if you create your key at the same level as other default keys like message and line.

GraphQL services should not provide any additional entries to the error format since they could conflict with additional entries that may be added in future versions of this specification.

https://facebook.github.io/graphql/June2018/#sec-Errors

The variety of errors you get with GraphQL

Alright, now we know how to express errors in GraphQL.
Next, let's see what kind of errors you can get with GraphQL.
I'm referencing the Apollo's article on error-handling.

Full Stack Error Handling with GraphQL + Apollo 🚀 – Apollo GraphQL

Those are the 2 perspectives we use to categorize errors.

  • Is it the client or server that is at fault?
  • Where did the error occur?

We can categorize client errors into 3 types.

  • parse error
    • query syntax error
  • validation error
    • error at the type check phase
  • execution error
    • authentication error

Server-side errors are the errors occurred in Rails API codes.

Now that we found out what kind of errors are out there, let's see how we can express them in responses.
We want to unify the format of all the responses as specified in the specification so that client tools like Apollo can parse them easily.
At this time, we will put detailed error messages into message key and put a key named code into extensions key which represents a status code.
This is actually the same format as that of the example response in the specification.

If you get an error at authentication phase, the response will look like this.

"errors": [
    {
      "message": "permission denied",
      "locations": [],
      "extensions": {
        "code": "AUTHENTICATION_ERROR"
      }
    }
  ]

If an error occurred in Rails API codes, the response will look like this.

"errors": [
    {
      "message": "undefined method 'hoge' for nil",
      "locations": [],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ]

The implementation with graphql-ruby

Now that you know how to express errors in responses, let's see how you can implement those with graphql-ruby.

how do you catch and return errors with graphql-ruby?

You can put an error into errors key in the response by raising GraphQL::ExecutionError like this.

def resolve(name:)
  user = User.new(name: name)
  if user.save
    { user: user }
  else
    raise GraphQL::ExecutionError, user.errors.full_messages.join(", ")
  end
end

Authentication error

Let's take a look at this example showing how to deal with an authentication error.

This example is based on the premise that you can get your login session by calling current_user method like this.

class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = { current_user: current_user }
    result = SampleSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
    #...
  end
  #...
end

Then you raise GraphQL::ExecutionError with the error code AUTHENTICATION_ERROR depends on the value of context[:current_user] in the resolve method.

def resolve(name:, sex:)
  raise GraphQL::ExecutionError.new('permission denied', extensions: { code: 'AUTHENTICATION_ERROR' }) unless context[:current_user]

  #...
end

With this implementation, you get a response like this when an authentication error occurred.

"errors": [
    {
      "message": "permission denied",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ],
      "extensions": {
        "code": "AUTHENTICATION_ERROR"
      }
    }
  ]

safety net with rescue_from method

As you may know by now, it is important to unify the format of responses so that the clients can parse them easily.
If an occurred error is not rescued, general 500 internal server error is returned to the clients which forces them to be prepared to get 2 types of responses.
Obviously you do not want that to happen because you want the clients to be comfortable with handling responses.
With graphql-ruby, you can make sure responses are in the same format with rescue_from method.

class SampleSchema < GraphQL::Schema
  rescue_from(StandardError) do |message|
    GraphQL::ExecutionError.new(message, extensions: {code: 'INTERNAL_SERVER_ERROR'})
  end

#...
end

The response will look like this.

"errors": [
    {
      "message": "hogehoge",
      "extensions":  {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ]

By the way, I wrote the patch to pass Error class objects to the method :)

extend GraphQL::Schema::RescueMiddleware#attempt_rescue by masakazutakewaka · Pull Request #2140 · rmosolgo/graphql-ruby · GitHub

Well it's not included in a stable blanch yet so technically you can only pass a String object to the method at the moment though...

Keep in mind that this rescue_from method is unavailable from version 1.9.
That is because GraphQL::Schema::RescueMiddleware class is not supported in version 1.9 which is where rescue_from method is defined.
GraphQL - Interpreter

You don't have a substitute for the method in version 1.9 and we don't even know what the additional feature to replace it might look like at the moment.
GraphQL::Execution::Interpreter and rescue_from compatibility · Issue #2139 · rmosolgo/graphql-ruby · GitHub

handling multiple errors with add_error method

You want to bundle up all the occurred errors in a response in some cases.
One of the cases is when you have a mutation with multiple user inputs like a sign up interface.
With graphql-ruby, you can bundle up multiple errors with add_error method.

module Mutations
  class CreateUser < GraphQL::Schema::RelayClassicMutation
    argument :name, String, required: true
    argument :sex, String, required: true

    field :user, Types::UserType, null: true

    def resolve(name:, sex:)
      user = User.new({ name: name, sex: sex })
      if user.save
        { user: user }
      else
        build_errors(user)
        return # rescue_from is invoked without this
      end
    end

    def build_errors(user)
      user.errors.map do |attr, message|
        message = user[attr] + ' ' + message
        context.add_error(GraphQL::ExecutionError.new(message, extensions: { code: 'USER_INPUT_ERROR', attribute: attr }))
      end
    end
  end
end

The response with multiple errors in it will look like this.

"errors": [
    {
      "message": "hoge already exists",
      "extensions": {
        "code": "USER_INPUT_ERROR",
        "attribute": "name"
      }
    },
    {
      "message": "fuge already exists",
      "extensions": {
        "code": "USER_INPUT_ERROR",
        "attribute": "sex"
      }
    }
  ]

On a side note, there is another way to achieve this which is to define the GraphQL type for the error and put errors in data key.
https://github.com/rmosolgo/graphql-ruby/blob/master/guides/mutations/mutation_errors.md#errors-as-data

conclusion

In this article, I introduced the idea on how error-handling is done in GraphQL and the implementation of it using graphql-ruby.
GraphQL's specification is put together well and not too long to read so I recommend you have a read.
Hopefully this article can be helpful for those who are grappling with error-handling in GraphQL right now.

Lastly, I'm looking for a job in Canada so any messages or contacts are more than welcome!

Email: takewakamma@gmail.com

As long as it's Canada, I can relocate :)

Thank you for reading!

references

Top comments (1)

Collapse
 
stefandorresteijn profile image
Stefan Dorresteijn

Thanks for the post, this is actually something I hadn't figured out yet for Rails!