In a previous post I walked you through building and consuming a GraphQL API in Ruby. That was the happy path. In this post we’ll generate and handle some errors.
Errors in a GraphQL API
GraphQL APIs can fail in three ways.
- A client-side error, eg. a query parsing error based on well-known schema.
- A server-side error, eg. a failure to create a database record behind the scenes.
- A transport error, eg. a server failing to respond with a 200 status to a
POST
query to the API.
Client-Side Errors
Client-side errors include parser and validation errors that don’t require a roundtrip to the server. Parsing an invalid query with any client library based on graphql-ruby will raise a GraphQL::ParseError
, see @d42b6238 for an example.
Server-Side Errors
For server-side implementations the graphql-ruby doc on errors gives a few options.
Returning Errors Inside Resolve
You can return a GraphQL::ExecutionError
inside a resolve
function, see @027d75cf for an example.
The response contains both a nil
value in data
and an errors
field with an error at the executionError
path with a message.
Handling Predictable Errors
You can turn typical errors, such as ActiveRecord::RecordNotFound
, ActiveRecord::RecordInvalid
or catch-all StandardError
into a GraphQL::ExecutionError
using a generic rescue object, see @a0b8f58b for an example.
This is used with resolve
as follows.
But for most APIs a catch-all at schema level is much easier than having to wrap each resolve
call into resolve Rescuable.new
, which can be done with the graphql-errors gem. See @9e65eb47.
Transport Errors
Transport errors are straightforward and depend on the GraphQL client and HTTP library used. For example Graphlient’s FaradayAdapter
uses Faraday::Response::RaiseError
, which will turn any non-200 HTTP status code into an exception, see adapters/http/faraday_adapter.rb#24.
Client-Side Errors or Exceptions?
This is often a matter of preference or strong opinions.
I believe that any unexpected behavior outside of a happy path should raise an exception unless it can be explicitly avoided ahead of time. I also prefer to deal with two scenarios rather than three: it worked and it didn’t work vs. it worked, it didn’t work in some predictable way and it didn’t work in some unpredictable way. Practically, this means I want to see a StandardError
when something failed, vs. having to check for an .errors
field in my business logic.
The Github graphql-client library does not raise exceptions except when otherwise for all types of errors and parses server-side errors in potentially problematic ways (see graphql-client#132), leaving you having to both handle exceptions and check for .data.errors
and .errors
.
The graphlient library built on top of graphql-client attempts to make sense out of the many scenarios and always raises exceptions of Graphlient::Errors::Error
variety for server-side problems. It’s simpler to use and requires less if
statements.