Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

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.

  1. A client-side error, eg. a query parsing error based on well-known schema.
  2. A server-side error, eg. a failure to create a database record behind the scenes.
  3. 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.

resolve ->(_object, _inputs, _ctx) {
  GraphQL::ExecutionError.new('This has not been implemented yet.')
}

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.

class Rescuable
  attr_reader :resolve_func

  def initialize(resolve_func)
    @resolve_func = resolve_func
  end

  def call(obj, args, ctx)
    resolve_func.call(obj, args, ctx)
  rescue ActiveRecord::RecordNotFound => e
    nil
  rescue ActiveRecord::RecordInvalid => e
    error_messages = e.record.errors.full_messages.join("\n")
    GraphQL::ExecutionError.new "Validation failed: #{error_messages}."
  rescue StandardError => e
    GraphQL::ExecutionError.new e.message
  end
end

This is used with resolve as follows.

field :findInvoiceById, InvoiceType do
  argument :id, !types.Int
  resolve Rescuable.new ->(_object, args, _ctx) {
    Invoice.where(id: args[:id]).first
  }
end

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.

GraphQL::Errors.configure(Schema) do
  rescue_from ActiveRecord::RecordNotFound do
    nil
  end

  rescue_from ActiveRecord::RecordInvalid do |e|
    error_messages = e.record.errors.full_messages.join("\n")
    GraphQL::ExecutionError.new "Validation failed: #{error_messages}."
  end

  rescue_from StandardError do |e|
    GraphQL::ExecutionError.new e.message
  end
end

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.