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 :)
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
- GraphQL
- API | Graphcool Docs
- Full Stack Error Handling with GraphQL + Apollo 🚀 – Apollo GraphQL
- graphql-ruby/overview.md at master · rmosolgo/graphql-ruby · GitHub
- graphql-ruby/execution_errors.md at master · rmosolgo/graphql-ruby · GitHub
- graphql-ruby/mutation_errors.md at master · rmosolgo/graphql-ruby · GitHub
Top comments (1)
Thanks for the post, this is actually something I hadn't figured out yet for Rails!