Error Handling in GraphQL-Ruby

Error handling in GraphQL can be challenging because GraphQL has its own error format and structure. However, with GraphQL-ruby, you can implement robust error handling that provides clear, actionable error messages to your API consumers.

GraphQL Error Format

GraphQL errors follow a specific structure:

{
  "errors": [
    {
      "message": "Error message",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["fieldName"],
      "extensions": {
        "code": "CUSTOM_ERROR_CODE"
      }
    }
  ],
  "data": null
}

Basic Error Handling

Raising Errors in Resolvers

The simplest way to handle errors is to raise exceptions:

# app/graphql/resolvers/find_user.rb
module Resolvers
  class FindUser < BaseResolver
    argument :id, ID, required: true

    def resolve(id:)
      user = User.find_by(id: id)

      raise GraphQL::ExecutionError, "User not found" unless user

      user
    end
  end
end

Using GraphQL::ExecutionError

GraphQL-ruby provides GraphQL::ExecutionError for custom errors:

def resolve(id:)
  user = User.find_by(id: id)

  if user.nil?
    raise GraphQL::ExecutionError.new(
      "User with ID #{id} not found",
      extensions: { code: "USER_NOT_FOUND" }
    )
  end

  user
end

Structured Error Handling

Custom Error Classes

Create custom error classes for better organization:

# app/graphql/errors/base_error.rb
module Errors
  class BaseError < GraphQL::ExecutionError
    def initialize(message, code: nil, field: nil)
      super(message, extensions: { code: code })
      @field = field
    end
  end
end

# app/graphql/errors/not_found_error.rb
module Errors
  class NotFoundError < BaseError
    def initialize(resource:, id:)
      super(
        "#{resource} with ID #{id} not found",
        code: "#{resource.upcase}_NOT_FOUND"
      )
    end
  end
end

# app/graphql/errors/validation_error.rb
module Errors
  class ValidationError < BaseError
    def initialize(errors)
      super(
        "Validation failed",
        code: "VALIDATION_ERROR"
      )
      @errors = errors
    end
  end
end

Using Custom Errors

def resolve(id:)
  user = User.find_by(id: id)
  raise Errors::NotFoundError.new(resource: "User", id: id) unless user
  user
end

Handling ActiveRecord Errors

Validation Errors

Handle ActiveRecord validation errors gracefully:

# app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < BaseMutation
    argument :email, String, required: true
    argument :name, String, required: true

    field :user, Types::UserType, null: true
    field :errors, [Types::ErrorType], null: false

    def resolve(email:, name:)
      user = User.new(email: email, name: name)

      if user.save
        { user: user, errors: [] }
      else
        {
          user: nil,
          errors: user.errors.full_messages.map do |message|
            { message: message, field: "user" }
          end
        }
      end
    end
  end
end

Using Error Types

Define an error type for structured errors:

# app/graphql/types/error_type.rb
module Types
  class ErrorType < Types::BaseObject
    field :message, String, null: false
    field :field, String, null: true
    field :code, String, null: true
  end
end

Global Error Handling

Rescue From Handler

Handle errors globally in your schema:

# app/graphql/your_schema.rb
class YourSchema < GraphQL::Schema
  rescue_from(ActiveRecord::RecordNotFound) do |err, obj, args, ctx, field|
    raise GraphQL::ExecutionError.new(
      "Record not found: #{err.message}",
      extensions: { code: "RECORD_NOT_FOUND" }
    )
  end

  rescue_from(ActiveRecord::RecordInvalid) do |err, obj, args, ctx, field|
    raise GraphQL::ExecutionError.new(
      "Validation failed: #{err.record.errors.full_messages.join(', ')}",
      extensions: { code: "VALIDATION_ERROR" }
    )
  end

  rescue_from(StandardError) do |err, obj, args, ctx, field|
    # Log error for debugging
    Rails.logger.error("GraphQL Error: #{err.message}")
    Rails.logger.error(err.backtrace.join("\n"))

    # Return generic error to client
    raise GraphQL::ExecutionError.new(
      "An error occurred",
      extensions: { code: "INTERNAL_ERROR" }
    )
  end
end

Error Codes

Define standard error codes:

# app/graphql/error_codes.rb
module ErrorCodes
  NOT_FOUND = "NOT_FOUND"
  VALIDATION_ERROR = "VALIDATION_ERROR"
  UNAUTHORIZED = "UNAUTHORIZED"
  FORBIDDEN = "FORBIDDEN"
  INTERNAL_ERROR = "INTERNAL_ERROR"
end

Use them consistently:

raise GraphQL::ExecutionError.new(
  "User not found",
  extensions: { code: ErrorCodes::NOT_FOUND }
)

Field-Level Errors

Add errors to specific fields:

# app/graphql/mutations/update_user.rb
module Mutations
  class UpdateUser < BaseMutation
    argument :id, ID, required: true
    argument :email, String, required: false

    field :user, Types::UserType, null: true
    field :errors, [Types::ErrorType], null: false

    def resolve(id:, email: nil)
      user = User.find(id)

      if email && User.exists?(email: email)
        return {
          user: nil,
          errors: [{
            message: "Email already taken",
            field: "email",
            code: "EMAIL_TAKEN"
          }]
        }
      end

      user.email = email if email

      if user.save
        { user: user, errors: [] }
      else
        {
          user: nil,
          errors: user.errors.map do |error|
            {
              message: error.full_message,
              field: error.attribute.to_s,
              code: "VALIDATION_ERROR"
            }
          end
        }
      end
    end
  end
end

Best Practices

  1. Use error codes: Provide consistent error codes for client handling
  2. Include field information: Specify which field caused the error
  3. Don't expose internals: Avoid leaking sensitive information
  4. Log errors: Log detailed errors server-side for debugging
  5. Handle gracefully: Always return a valid GraphQL response

Example: Complete Error Handling

# app/graphql/mutations/create_post.rb
module Mutations
  class CreatePost < BaseMutation
    argument :title, String, required: true
    argument :content, String, required: true

    field :post, Types::PostType, null: true
    field :errors, [Types::ErrorType], null: false

    def resolve(title:, content:)
      # Authorization check
      unless context[:current_user]
        return {
          post: nil,
          errors: [{
            message: "Authentication required",
            code: ErrorCodes::UNAUTHORIZED
          }]
        }
      end

      post = context[:current_user].posts.build(
        title: title,
        content: content
      )

      if post.save
        { post: post, errors: [] }
      else
        {
          post: nil,
          errors: post.errors.map do |error|
            {
              message: error.full_message,
              field: error.attribute.to_s,
              code: ErrorCodes::VALIDATION_ERROR
            }
          end
        }
      end
    rescue StandardError => e
      Rails.logger.error("Error creating post: #{e.message}")
      {
        post: nil,
        errors: [{
          message: "Failed to create post",
          code: ErrorCodes::INTERNAL_ERROR
        }]
      }
    end
  end
end

Conclusion

Error handling in GraphQL-ruby requires understanding GraphQL's error format and using the right tools. By implementing structured error handling with custom error classes, error codes, and global handlers, you can build robust GraphQL APIs that provide clear, actionable error messages to your clients.

Error Handling in GraphQL-Ruby - Abhay Nikam