Skip to main content

Overview

Rails GraphQL provides robust error handling that follows the GraphQL specification. Errors during field processing produce partial responses with detailed error information under the errors key.
Important: Exceptions are always captured and added to errors unless you return false from a rescue_from handler.

Error Response Structure

Per the GraphQL Spec, errors produce partial responses:
{
  "data": {
    "user": null,
    "posts": [
      { "id": "1", "title": "Hello" }
    ]
  },
  "errors": [
    {
      "message": "User not found",
      "path": ["user"],
      "locations": [{ "line": 2, "column": 3 }],
      "extensions": {
        "exception": "ActiveRecord::RecordNotFound"
      }
    }
  ]
}

Adding Errors

The request provides three methods for adding errors:

exception_to_error

Converts exceptions to error responses:
def exception_to_error(exception, node, **extra)
  # Adds exception to extensions
  # Gets message from exception if not provided
end
begin
  User.find(id)
rescue ActiveRecord::RecordNotFound => e
  request.exception_to_error(e, field)
end

report_node_error

Reports errors for specific nodes:
def report_node_error(message, node, **extra)
  # Adds location from node if not provided
end
request.report_node_error(
  "Invalid input",
  field,
  extensions: { code: "VALIDATION_ERROR" }
)

report_error

Simplest error reporting method:
def report_error(message, **extra)
  # Adds current stack as path if not provided
end
report_error returns nil, making it perfect for resolver return values:
def welcome
  return request.report_error('Not authorized') unless authorized?
  # No need for explicit nil return
end
def create_user
  request.report_error("Email already taken")
end

Using rescue_from

Schemas implement ActiveSupport::Rescuable for exception handling:
# From lib/rails/graphql/schema.rb
rescue_from(PersistedQueryNotFound) do |error|
  response = { errors: [{ message: +'PersistedQueryNotFound' }] }
  error.request.force_response(response, error)
end

Exception Context

Rescued exceptions have request context:
args
Hash
Arguments passed to the request
source
Object
Component where the error occurred
request
Request
The request instance
response
Response
The response instance
document
Document
The parsed GraphQL document

Custom Error Handlers

# app/graphql/app_schema.rb
rescue_from(ActiveRecord::RecordInvalid) do |error|
  request.report_error(
    error.message,
    extensions: {
      code: "VALIDATION_ERROR",
      errors: error.record.errors.as_json
    }
  )
end

Skipping Error Reporting

Return false to skip automatic error handling:
rescue_from(SilentError) do |error|
  # Log it but don't add to response
  Rails.logger.warn("Silent error: #{error.message}")
  false  # Prevents automatic error addition
end

Console Display

Errors produce formatted output in the server console:
# app/graphql/app_schema.rb
field(:welcome).resolve { raise 'Something went wrong.' }
Console output:
   | Loc | Field          | Object | Arguments | Result
---+-----+----------------+--------+-----------+--------------------
 1 | GQL | schema         | :base  |           | GraphQL::AppSchema
 2 | 1:1 | query          | nil    | {}        | _Query
 3 | 1:3 | _Query.welcome | nil    | {}        | ↓

Something went wrong. (RuntimeError) [resolve]
/home/app/graphql/app_schema.rb:3:in `block in <class:AppSchema>'
The table shows the execution path leading to the error, making debugging easier.

Table Columns

Loc
string
Location in GraphQL document or “GQL” for internal
Field
string
Field name or operation type
Object
string
Parent object or type
Arguments
Hash
Arguments passed to the field
Result
any
Field result or ”↓” if error occurred

Error Categories

Validation Errors

field(:create_user).perform do |input:|
  user = User.new(input)
  unless user.valid?
    return request.report_error(
      "Validation failed",
      extensions: { errors: user.errors.as_json }
    )
  end
  user.save!
end

Authentication Errors

field(:current_user).resolve do
  return request.report_error("Not authenticated") unless context[:user]
  context[:user]
end

Authorization Errors

field(:delete_post).authorize do
  unless current_user.can?(:delete, post)
    request.report_error(
      "Unauthorized",
      extensions: { code: "FORBIDDEN" }
    )
    false  # Stops execution
  end
end

Business Logic Errors

field(:transfer_funds).perform do |from:, to:, amount:|
  if from.balance < amount
    return request.report_error(
      "Insufficient funds",
      extensions: {
        code: "INSUFFICIENT_FUNDS",
        available: from.balance,
        requested: amount
      }
    )
  end
  
  from.withdraw(amount)
  to.deposit(amount)
end

Best Practices

Use Specific Handlers

Create specific rescue_from handlers for different error types:
rescue_from(ActiveRecord::RecordNotFound) { |e| handle_not_found(e) }
rescue_from(ValidationError) { |e| handle_validation(e) }
rescue_from(AuthError) { |e| handle_auth(e) }

Include Error Codes

Add machine-readable codes to extensions:
extensions: {
  code: "VALIDATION_ERROR",
  field: "email"
}

Provide Context

Include relevant details in error extensions:
extensions: {
  attempted_action: "delete_post",
  required_role: "admin",
  user_role: "user"
}

Log Appropriately

Log errors before reporting:
rescue_from(CriticalError) do |error|
  Rails.logger.error("Critical: #{error.message}")
  request.report_error("An error occurred")
end

Testing Error Handling

RSpec.describe GraphQL::AppSchema do
  it "handles validation errors" do
    result = execute(<<~GQL, user: nil)
      mutation {
        createUser(input: { email: "invalid" }) {
          id
        }
      }
    GQL
    
    expect(result[:errors]).to be_present
    expect(result[:errors][0][:extensions][:code]).to eq("VALIDATION_ERROR")
  end
end

Request

Request lifecycle and processing

Events

Event system for error handling

Build docs developers (and LLMs) love