It is a known problem how GraphQL breaks the widely known HTTP status code for error handling. Everything either responds with a 200 or a 500. There are however, a couple of clever ways to go around this problem and utilise the GraphQL type system to properly handle errors. In this article, I will like to document how I handle errors in GraphQL in Ruby. This particular technique is in no way mine, the original source explaining the technique can be found here. After trying to implement the same technique in Ruby, I came across several problems, then I decided to document & share them through this article. For the purpose of this article, I will be building a hypothetical API to manage employees of a company.

Defining your Union Classes

Lets define our Union types to encapsulate an error type for a particular mutation or query resolver. For our employee resource, we define a EmployeeResult as below:

# app/graphql/types/unions/employee_result.rb
module Types
  module Unions
    class EmployeeResult < BaseUnion
      description 'Employee possible responses'
      possible_types Types::Objects::EmploymentType,
                     Types::Errors::CompanyNotFoundError,
                     Types::Errors::BankNotFoundError,
                     Types::Errors::EmploymentExistsError,
                     Types::Errors::AuthenticationError
    end
  end
end
# app/graphql/types/errors/company_not_found_error.rb
module Types
  module Errors
    class CompanyNotFoundError < Types::Objects::BaseObject
      implements Types::Interfaces::ErrorType

      def message
        'Company does not exist'
      end
    end
  end
end

Now, every mutation and resolver class that returns a Union type can potentially return all objects defined with the possible_types method above. However, when we return an object from our resolver classes, it is not enough for us to return a __typename field to specify the type the object should be resolved into. graphql-ruby provides us with a method it can hook into to provide this resolution of an object to it's concrete type, this method can be defined both on the actual union type class and the superclass. I define here it in the superclass, see below:

# app/graphql/types/unions/base_union.rb
module Types
  module Unions
    class BaseUnion < GraphQL::Schema::Union
      def self.resolve_type(object, _context)
        name = object[:__typename] || object.class.name
        ('Types::Objects::' + name + 'Type').constantize
      rescue NameError => e
        ('Types::Errors::' + object[:__typename]).constantize
      end
    end
  end
end

The resolve_type function above defines the resolution logic. It should return the class object that defines the concrete type to be returned. For non-error types, it can either be defined in the __typename field as well as using the using the object class name to assume the type. For error types, it is totally dependent on the __typename field to resolve to its concrete type.

Defining your Mutation

For the mutation definition it changes from left to right:

# Non Union result.
module Mutations
  class CreateEmployee < BaseMutation
    argument :name, String, 'Employee name', required: true
		...

    field :company, Types::Objects::CompanyType, null: false

    def resolve(name:)
			...
			# Create employee.
      { employee: employee }
    end
  end
end
# Using Union.
module Mutations
  class CreateEmployee < BaseMutation
		argument :name, String, 'Employee name', required: true
		...

		payload_type Types::Unions::EmployeeResult
    
    def resolve(name:)
			...
			# Create employee.
			return { __typename: 'CompanyNotFoundError' } if company.nil?
      employee
    end
  end
end

While the code on the left specifies the concrete type returned by the mutation resolver, the code on the right specifies the union type, and specifically can return an object specifying an error object or the object in itself.

Defining your Resolvers

For the query resolver classes, it changes from left to right:

module Resolvers
	class GetEmployeesResolver < Base
	  type [Types::Objects::EmploymentType], null: false

	  def resolve
			...
		  raise GraphQL::ExecutionError, 'Company does not exist' unless company

	    company.employments
	  end
	end
end
module Resolvers
  class GetEmployeesResolver < Base
    type [Types::Unions::EmployeeResult], null: false

    def resolve
			...
	    return { __typename: 'CompanyNotFoundError' } unless company

     company.employments.where(is_active: true)
    end
  end
end

Again, the code on the left specifies an array of concrete types to be resolved, while the code on the right resolves to an array of union types.

Caveat on handling Authentication Errors.

Everything up till now, works fine. But there is a caveat on handling authentication errors, @timigod wrote a nice write up here on write using an AuthorizeUser helper to handle authentication in Ruby GraphQL. This solution uses something similar to the below: