Error Handling in GraphQL

Introduction

Welcome to the lesson on best practices for error handling in GraphQL. In this lesson, we will explore how to handle errors effectively in your GraphQL API using graphql-ruby and Sinatra. Proper error handling is crucial for building reliable and user-friendly applications.

How GraphQL Handles Errors and Common Error Types

GraphQL treats errors as part of the response format. If any field in a query fails, it includes an errors array in the response. Common error types include:

  • User input errors.
  • Authentication errors.
  • Validation errors.
  • System errors.

Implementing Basic Error Handling in Resolvers

graphql-ruby provides built-in error handling through GraphQL::ExecutionError to help you handle common errors. Let's start with handling missing data and validating user inputs.

Here's how you can raise a GraphQL::ExecutionError if a book is not found:

Ruby
require 'graphql'

BOOKS = [
  { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' },
  { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' }
]

class BookType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :author, String, null: false
end

class QueryType < GraphQL::Schema::Object
  field :book, BookType, null: true do
    argument :id, ID, required: true
  end

  def book(id:)
    book = BOOKS.find { |b| b[:id] == id }
    raise GraphQL::ExecutionError, 'Book not found' unless book
    book
  end
end

In this code, we first define BookType with fields id, title, and author. Then, in QueryType, when a book with the specified id is not found, a GraphQL::ExecutionError is raised with the message "Book not found."

Example: Validating User Inputs

You can also raise an error if the required inputs are missing or invalid:

Ruby
class MutationType < GraphQL::Schema::Object
  field :add_book, BookType, null: false do
    argument :title, String, required: true
    argument :author, String, required: true
  end

  def add_book(title:, author:)
    if title.nil? || title.empty? || author.nil? || author.empty?
      raise GraphQL::ExecutionError, 'Title and Author are required'
    end
    
    new_book = {
      id: (BOOKS.length + 1).to_s,
      title: title,
      author: author
    }
    BOOKS << new_book
    new_book
  end
end

In this code, if either title or author is missing or empty, a GraphQL::ExecutionError is raised with a relevant message.

Advanced Error Handling Techniques

For more complex cases, you might want to create custom error classes that inherit from GraphQL::ExecutionError:

Ruby
class MyCustomError < GraphQL::ExecutionError
  def initialize(message)
    super(message)
  end
end

You can then use this custom error class in your resolvers:

Ruby
def book(id:)
  book = BOOKS.find { |b| b[:id] == id }
  raise MyCustomError, 'Book not found' unless book
  book
end

Handling Multiple Errors

You can handle multiple errors by creating a list of errors and raising them as needed:

Ruby
def add_book(title:, author:)
  errors = []
  errors << 'Title is required' if title.nil? || title.empty?
  errors << 'Author is required' if author.nil? || author.empty?
  
  raise GraphQL::ExecutionError, errors.join(', ') unless errors.empty?
  
  new_book = {
    id: (BOOKS.length + 1).to_s,
    title: title,
    author: author
  }
  BOOKS << new_book
  new_book
end

Example: Error Handling in a Complete Application

Now, let's put it all together in a complete example.

Here is our server code:

Ruby
require 'sinatra'
require 'graphql'
require 'json'

BOOKS = [
  { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' },
  { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' }
]

class BookType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :author, String, null: false
end

class QueryType < GraphQL::Schema::Object
  field :book, BookType, null: true do
    argument :id, ID, required: true
  end

  def book(id:)
    book = BOOKS.find { |b| b[:id] == id }
    raise GraphQL::ExecutionError, 'Book not found' unless book
    book
  end
end

class MutationType < GraphQL::Schema::Object
  field :add_book, BookType, null: false do
    argument :title, String, required: true
    argument :author, String, required: true
  end

  def add_book(title:, author:)
    if title.nil? || title.empty? || author.nil? || author.empty?
      raise GraphQL::ExecutionError, 'Title and Author are required'
    end
    
    new_book = {
      id: (BOOKS.length + 1).to_s,
      title: title,
      author: author
    }
    BOOKS << new_book
    new_book
  end
end

class AppSchema < GraphQL::Schema
  query QueryType
  mutation MutationType
end

post '/graphql' do
  request_payload = JSON.parse(request.body.read)
  query = request_payload['query']
  variables = request_payload['variables'] || {}
  
  result = AppSchema.execute(query, variables: variables)
  content_type :json
  result.to_json
end

set :port, 4000

And here is how we make queries for the server:

Ruby
require 'net/http'
require 'json'
require 'uri'

url = URI.parse('http://localhost:4000/graphql')

def fetch_book(url, id)
  query = <<~GRAPHQL
    query {
      book(id: "#{id}") {
        title
        author
      }
    }
  GRAPHQL
  
  http = Net::HTTP.new(url.host, url.port)
  request = Net::HTTP::Post.new(url.path, { 'Content-Type' => 'application/json' })
  request.body = { query: query }.to_json
  
  response = http.request(request)
  data = JSON.parse(response.body)
  
  if data['errors']
    puts 'Errors:', data['errors']
  else
    puts 'Data:', data
  end
rescue StandardError => e
  puts 'Network Error:', e.message
end

def add_new_book(url, title, author)
  mutation = <<~GRAPHQL
    mutation {
      addBook(title: "#{title}", author: "#{author}") {
        id
        title
        author
      }
    }
  GRAPHQL
  
  http = Net::HTTP.new(url.host, url.port)
  request = Net::HTTP::Post.new(url.path, { 'Content-Type' => 'application/json' })
  request.body = { query: mutation }.to_json
  
  response = http.request(request)
  data = JSON.parse(response.body)
  
  if data['errors']
    puts 'Errors:', data['errors']
  else
    puts 'Data:', data
  end
rescue StandardError => e
  puts 'Network Error:', e.message
end

# Test the functions
fetch_book(url, '1')
add_new_book(url, '1984', 'George Orwell')

In this code, errors from the server response are logged to the console using puts. Additionally, any network errors during the HTTP request are caught using Ruby's rescue clause and logged as "Network Error."

When you run this code, you will set up a server that handles both queries and mutations with proper error handling.

Lesson Summary

To summarize, in this lesson, you learned the importance of error handling in GraphQL, how to implement basic and advanced error handling techniques using graphql-ruby, and saw how to apply them in a complete application with Sinatra.

As you move on to the practice exercises, apply these techniques to solidify your understanding. Keep practicing and explore more advanced topics to enhance your expertise in building secure and resilient GraphQL APIs.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal