Nullability in GraphQL is a controversial topic. Some say constant null checks are a nuisance, while others err on the side of paranoia. I say, UFO's are real. But, that's not the point. In this post, you'll hear both sides of the discussion, find a cheatsheet you can refer to later, and read about different approaches to nulls and errors by Apollo and Relay.
- Nullability and error handling in GraphQL
- Sending nulls in GraphQL queries
- Receiving nulls in GraphQL responses
- Finding the true meaning of null
- Anatomy of GraphQL errors
- Pros & cons
- Different approaches by GraphQL clients
Nullability and error handling in GraphQL
So what's all the ruckus about? It all starts with GraphQL's unconventional approach to defaults and error handling.
In other typed languages, like TypeScript, Flow, Rust, or Haskell, all types are non-nullable by default, and you opt into nullability. But in GraphQL, all types are nullable by default, and you opt into non-nullability. In fact, Non-Null
is a type that wraps your original type when you opt in.
TypeScript:
interface User {
name?: string // opting into optional name
name: string | null // opting into nullable name
}
GraphQL:
type User {
name: String! // opting into non-nullable name
}
As if that's not enough fodder for DRAMZ, GraphQL breaks with the REST convention of relying on HTTP status codes for error handling. Even if there's an error in the GraphQL layer while executing your request, the response is still 200 OK
š²
Then how does the client know about errors? This is where null
s become relevant. The server returns a root field called errors
, from which the client can extract them, and a root field called data
that has all the requested fields. To top it off, any fields with errors have the value null
.
All of this to say: all types in GraphQL are nullable by default in case of an error. GraphQL doesn't let a small hiccup get in the way of our data fetching: when there's an error with a field, we still get partial data.
We'll debate the pros and cons in a bit, including different approaches by Apollo and Relay. But first, the promised cheatsheet...
Nulls in the query
A GraphQL query can have fields and inputs, with or without variables.
Fields are always optional, whether nullable or not.
type User {
name: String! // non-null
age: Int // nullable
}
{
user {
name // optional
age // optional
}
}
In inputs, such as field arguments, nullable types are always optional and nonānull types are always required.
type User {
// nullable filter input, non-null size input
imageUrl(filter: String, size: String!): String
}
{
user {
imageUrl // not valid
imageUrl(size: null) // not valid
imageUrl(size: 'med') // valid
imageUrl(filter: null, size: 'med') // valid
imageUrl(filter: 'juno', size: 'med') // valid
}
}
In addition, a variable of a nullable type cannot be provided to a nonānull argument.
query user($imageSize: String) { // nullable variable
imageUrl(size: $imageSize) // field with non-null argument
}
Nulls in the response
If you get a null
value for a nullable field from the server, there are four possibilities:
- The value is actually null or absent, e.g. user doesn't exist
- The value is hidden, e.g. user has blocked you (permission error)
- There was another error fetching the value
- An error or
null
value has bubbled up from aNon-Null
child
Wait, what was that about bubbling up?
The GraphQL spec says that a null
result on a Non-Null
type bubbles up to the next nullable parent. If this bubbling never stops because everything is of Non-Null
type, then the root data
field is null
.
Let's say name
is nullable, but the resolver returns an error while fetching it:
data: {
user: {
name: null,
age: 25,
// other fields on user
}
},
errors: [
{
message: "Name could not be fetched",
// ...
}
]
If name
is non-nullable and user
is nullable, this is what happens instead:
data: {
user: null
},
errors: [
{
message: "Name could not be fetched",
// ...
}
]
When fields are nullable, as in the first example, you still get partial data: even if you don't have the name, you can still show the age and other fields. When you mark fields as Non-Null
, as in the second example, you forfeit your right to partial data.
How can I know the true meaning of null
?
The four possibilities of null
leave us with three questions:
- Do we have an actual absent value or an error?
- If it's an error, which field is it on?
- What kind of error is it?
And the answers lie in the errors
list returned in the response.
If an error is thrown while resolving a field, it should be treated as though the field returned
null
, and an error must be added to theerrors
list in the response.
Anatomy of the errors
list
The errors
list must include message
, locations
, and path
entries, with an optional extensions
entry.
"errors": [
{
// error message from the resolver
"message": "Name for user with ID 42 could not be fetched.",
// map to the field in the GraphQL document
"locations": [{ "line": 3, "column": 5 }],
// path to the field
"path": ["user", 0, "name"],
// optional map with additional info
"extensions": {
code: PERMISSION_ERROR,
weatherOutside: 'weather'
}
}
]
The path
entry answers our first two questions: whether the null
result is intentional or due to an error, and what field it's on. The extensions
entry answers our third question by allowing us to specify the type of error, and anything else we can possibly divine: the time of day when it happened, the weather outside, etc.
In the case of a Non-Null
field, the path
entry still specifies the correct source of the error, even when the data
returned points to the parent as the troublemaker.
data: {
user: null
},
errors: [
{
message: "Name could not be fetched",
path: ["user", 0, "name"]
// ...
},
// ...
]
Pros & cons
Now that we've got the cheatsheet down, let's talk about the pros and cons of nullable vs Non-Null
types.
Benefits of nullable types
We've already covered one major benefit:
- When the HTTP status code is
200 OK
, we're able to get partial data from the server, despite errors on specific fields. But we still need a way to tell if something went wrong, which we can achieve with anull
value on the erroneous field, along with anerrors
list (Btw, we'll discuss another solution to this ā returning anError
type instead ofnull
ā in the Relay section below).
Other benefits:
-
Privacy when you want to obfuscate the reasons for
null
. Maybe you don't want the client to know whether you got anull
onuser
because the user has blocked you or because the user simply doesn't exist. If you're a ghoster, this is the way to go. - If you're serving different clients from a single schema, nullable fields are more reliable, and easier to evolve. For example, if you remove a
Non-Null
field from the schema, a client that you have no control ever may break when it receivesnull
for that field. - Coding defensively on the client side. Null check all the things!
if (user && user.location) return user.location.city;
Benefits of Non-Null
types
If you find null checks cumbersome, Non-Null
is the type for you. Why?
- You get guaranteed values. If you know that
location
is non-nullable, you can just do this:
if (user) return user.location.city;
If location returns null
it will bubble up to user
, and user.location.city
will never execute. location
is guaranteed to never be null
, so you never need a null check on it.
- You can combine
Non-Null
types with query type generation to make your code even more predictable. For example, with TypeScript, the generated code for a users query can be:
type GetUsersQuery = {
users: Array<{
__typename: "User",
name: string // if Non-Null in schema
name: string | null // if nullable in schema
}
};
-
Easy error detection. If you get a
null
value for a non-nullable field, you know it's because of an error, since it can't be a legitimate absent value.
So how should I design my schema?
Though this is a hotly debated topic, the general recommendation is to make everything nullable except when it doesn't make sense, e.g. getUserById(id: ID!)
, or the value is guaranteed to exist, e.g. a primary key like id
. But you can weigh the pros and cons above, and decide for yourself! (If you do, please let me know in the comments).
Different approaches by GraphQL clients
Depending on what GraphQL client you're using, you may be able to pick your strategy on a per-query basis, or come up with a different solution to error handling altogether.
Apollo: Everything is possible
When it comes to error handling, Apollo gives you options. Three options, in fact.
-
none
(default): Treat GraphQL errors like network errors, ignore data -
ignore
: Get data, ignore errors -
all
: Get both data & errors
The all
option matches the GraphQL spec, and will allow you to get partial data in case of an error. You can set the errorPolicy
on a per-query basis, like so:
const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: 'all' });
Relay: All is good all the time
Relay is all about living the good life. A little GraphQL error here and there isn't enough to mess with Relay's zen thing.
That's right. Relay ignores GraphQL errors unless:
- the fetch function provided to the Relay Network throws or returns an Error
- the top-level
data
field isn't returned in the response
The Relay docs recommend modeling errors in your schema rather than returning null
.
type Error {
message: String!
}
type User {
name: String | Error
}
For a detailed breakdown of how you might model this in your schema, check out this excellent blog post by Laurin Quast.
Conclusion
I hope this article has helped you master nulls in GraphQL. We covered:
- GraphQL's approach to nullability and error handling
- What nulls mean in GraphQL queries and responses
- Pros & cons of nullable and Non-Null types
- Different approaches to nulls and error handling by Apollo and Relay
Where do you stand in this discussion? What has been your experience with nullability? Let us know on Twitter or in the comments below!
Further Reading
- Using nullability in GraphQL, by Sashko Stubailo
- Nullability in GraphQL, by Grant Norwood
- When To Use GraphQL Non-Null Fields, by Caleb Meredith
- Handling GraphQL errors like a champ with unions and interfaces, by Laurin Quast
- GraphQL Best Practices: Nullability
- GraphQL Spec
- Handling errors with Apollo
- Relay: Accessing errors in GraphQL Response
Top comments (0)