Should every error result in an error in GraphQL, too?
- Published at
- Updated at
- Reading time
- 3min
I watched some Graphql Summit videos and the talk 200 OK! Error Handling in GraphQL by Sasha Solomon caught my eye. She shares an approach to GraphQL error handling that made me rethink how to model GraphQL APIs.
Let's have a look at one of her example queries.
{
user(id: "someId") {
id name
}
}
This query requests a user field with a specific id. The question is: what should happen when the user doesn't exist? A GraphQL response includes a data
and an optional errors
property.
One option to treat the non-existing user is a "classical error" added to the errors
object.
{
"data": {
"user": null
}
"errors": {
{
"path": ["user"],
"location": [{ "line": 2, "column": 3 }],
"extensions": {
"message": "object not found"
}
}
}
}
While this works, it feels like a mapping coming from a RESTful world to GraphQL. In this world, not found resources result in a 404
status code โ a red status code.
This approach has the disadvantage that errors show up in a different location โ they're in errors
instead of data
. That's not ideal and annoying to implement from an application side.
The question rises if you should treat a 404
(or any other expected error) like a hard error such as a very bad 500 โ something went wrong
after all? What if the GraphQL schema could define expected errors not as errors, but as different response types?
The significant advantage of GraphQL is that you write the query that shapes the responded data. What if you could control the errors that you want to receive, too? To make that work, Sasha shares the following approach.
When building a GraphQL API, you could stop querying for resources but start querying for results. A GraphQL API can define different resources for a single query field using union types. Let's have a look at how this works.
union UserResult = User | NotFoundUser
The schema definition above defines that the union type UserResult
can result in a User
or a NotFoundUser
. This definition allows you to adjust your query to treat the userResult
differently depending on its type.
The following query requests two users with different ids. One of the query fields results in an error.
{
userOne: userResult(id: "someId") {
__typeName
... on User {
id
name
}
... on NotFoundUser {
message
}
}
userTwo: userResult(id: "someOtherId") {
__typeName
... on User {
id
name
}
... on NotFoundUser {
message
}
}
}
Because the userResult
response varies, you have to use conditional fragments (
and
) to specify what data you're interested in depending on its type.
A possible response to this query could look like this.
{
"data": {
"userOne": {
"__typeName": "User",
"id": "someId",
"name": "Jane Doe"
},
"userTwo": {
"__typeName": "NotFoundUser",
"message": "User is out for lunch"
}
}
}
By returning a User
or a NotFoundUser
, you can include the error response right in the query field that requests the data. The error is not included in the errors
property.
The __typeName
field gives you the information about what object you're dealing with to adjust your application. That's pretty neat!
Advantages of error results
I have to say this approach seems very logical to me. Her described approach to GraphQL error handling has a few advantages.
Error results keep the GraphQL query in control of the response, even in an error case. You are 100% in control of the result, whether it's a successfully queried resource or an expected error. Additionally, you can decide what error results and what details you care about and which to ignore. The query stays in control, and that's what GraphQL is about after all.
Here's the YouTube video. Highly recommended!
Oh hey, and I just learned that Sasha wrote about it on Medium, too.
Join 5.5k readers and learn something new every week with Web Weekly.