200 OK! Error Handling in GraphQL

How to effectively model “errors” in your GraphQL schema

{
user(username: "@ash") {
id
name
}
}
{
"data": {
"user": {
"id": "268314bb7e7e",
"name": "Ash Ketchum"
}
}
}
{
"data": {
"user": null
},
"errors": [
{ "path": [ "user" ],
"locations": [ { "line": 2, "column": 3 } ],
"extensions": {
"message": "Object not found",
"type": 2
}
}
]
}

The Problem

So, what’s wrong with this?

  1. It’s hard to know where the error came from
    Our example was a simple query, but in more complex queries it’s harder to know where the error came from, especially if it’s, say, a list of items.
  2. It’s hard for the client to know what errors to care about
    Clients get all errors in the errors array, so clients can’t even query for errors. This means clients don’t know what cases there even are to handle, let alone which ones are important, which ones we can ignore, etc.

What is an error?

Before we really dig into this, we should really understand what an error is.

Error Categories

When we start thinking of “errors” in this way, we can separate them into categories.

Errors

If we make a request to our GraphQL server, our server will make subsequent calls to our backends/services. If one of our services throws an exception, we’ll get a HTTP 500 (or equivalent) back, and something like “Server Error” will end up in our errors array.

Alternative Results

For “alternative results”, this looks a little different. We have our client and our client hits the /graphql endpoint. When our graphql server calls out to our services, our services might send something back like Unavailable in Country or Suspended User.

Modeling Results in the GraphQL Schema

So how would we model this?

type User {
id: ID!
name: String
}
type Suspended {
reason: String
}
type IsBlocked {
message: String
blockedByUser: User
}
type UnavailableInCountry {
countryCode: Int
message: String
}
"User, or reason we can't get one normally"
union UserResult = User | IsBlocked | Suspended
{
userResult(username: "@ash") {
__typename
... on User {
id
name
}
... on IsBlocked {
message
blockedByUser {
username
}
}
... on Suspended {
reason
}
}
{
"data": {
"userResult": {
"__typename": "User",
"id": "268314bb7e7e",
"name": "Ash Ketchum"
}
}
}
{
"data": {
"userResult": {
"__typename": "IsBlocked",
"message": "User blocked: @ash",
"blockedByUser": {
"username": "@brock"
}
}
}
}
  1. We know where the error came from
    We know exactly where our error comes from in the query (because it’s attached to the entity); it’s actually encoded in the schema.
  2. The client decides what errors it cares about and what errors it can ignore
    The client can query or not query for different Results, so it decides what’s important.

Complex Schema Structures

If we use this way of modeling our Results, we can imagine using Results for different entities within the same query.

type ProfileImage {
id: ID!
value: String
}
type Flagged {
reason: String
}
type Hidden {
message: String
}
"Image, or reason we can't get one normally"
union ImageResult = Image | Flagged | Hidden
{
userResult(username: "@ash") {
__typename
... on User {
id
name
profileImageResult {
__typename
... on Image {
id
}
... on Flagged {
reason
}
... on Hidden {
message
}
}
}
... on IsBlocked {
message
blockedByUser {
username
}
}
}
{
"data": {
"userResult": {
"__typename": "User",
"id": "268314bb7e7e",
"name": "Ash Ketchum",
"profileImageResult": {
"__typename": "Image",
"id": "982642"
}
}
}
}
{
"data": {
"userResult": {
"__typename": "User",
"id": "268314bb7e7e",
"name": "Ash Ketchum",
"profileImageResult": {
"__typename": "Flagged",
"reason": "Copyrighted image"
}
}
}
}
  1. We can tune how verbose we want results to be
    We can add result types to any entity we want (like we did with User and ProfileImage) or NOT! We get to decide. New result types get added to the schema explicitly instead of sneaking in silently as new kinds of errors.
  2. We can more accurately represent our data
    Our schema structure mirrors what our data looks like, which makes it easier to reason about.

Results

Some things to take away when thinking about modeling “errors” in GraphQL, and just modeling in general:

  1. Model your errors as errors, model your actual results as results
  2. Results are usually things you want to display

--

--

software engineer @twitter, previously @medium. doing scala + graphql. pokemon gym leader. potato compatible. @sachee

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Sasha Solomon

software engineer @twitter, previously @medium. doing scala + graphql. pokemon gym leader. potato compatible. @sachee