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
- Use error codes: Provide consistent error codes for client handling
- Include field information: Specify which field caused the error
- Don't expose internals: Avoid leaking sensitive information
- Log errors: Log detailed errors server-side for debugging
- 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.