When To Use GraphQL Non-Null Fields

Learn what some of the unexpected costs of GraphQL non-null fields are.

In the GraphQL type system all types are nullable by default. This means that a type like Int can take any integer (1, 2, etc.) or null which represents the absence of any value. However, the GraphQL type system allows you to make any type non-null which means that the type will never produce a null value. When using a non-null type there will always be a value. A GraphQL non-null field is a GraphQL field where the type is non-null.

Non-null fields in GraphQL may seem great at first, after all they guarantee that you will always have a given field. However, using a non-null field may make it harder to evolve your GraphQL schema, and allow small errors to propagate in such a way that you lose important data which may have been returned if the field were nullable. This post explains the unexpected costs of non-null fields so that you can make informed choices when designing your schemas. Before we look at those costs, let’s take a closer look at some of the benefits of non-null fields from a UI developer’s perspective.

Non-null fields are a very powerful tool as they give you static guarantees that data will always exist. Being able to generate clean static types from your non-nullable fields is very attractive! For example, let’s take the following query:

{
name
age
image {
url
width
height
}
}

If all the fields in this query were non-null then you might generate a Flow type that looks like this for the query:

type Query = {
name: string,
age: number,
image: {
url: string,
width: number,
height: number,
},
};

However, if all the fields were nullable then the Flow type would look like this:

type Query = {
name: string | null,
age: number | null,
image: {
url: string | null,
width: number | null,
height: number | null,
} | null,
};

The first type looks just like a type you would write yourself, but the second requires a lot of runtime type checks to guarantee you have the right data available for render. (Even if you are using a dynamic language without typings, like JavaScript, it is useful to know what fields may be null.) As such nullable fields have an obvious cost to a UI developer.

However, there are some non-obvious costs to using non-null fields which may be greater in the long run then the cost of a nullable field. However, before we start looking at the costs, let’s discuss the history and semantics of non-null types in the GraphQL language.

The semantics of non-null types

The first version of GraphQL at Facebook did not support non-null fields at all. Every field was nullable and it was only in the open sourcing of the specification was the ability added to mark a field as non-null.

Even after the open sourcing of the GraphQL specification, you will notice that nullable fields are the default. Non-null types, on the other hand, are a second class citizen of the specification. In order to mark a field as non-null you must go out of your way to add a bang character (!).

type Query {
# I may return a user, or I may return null.
# That’s the default when you write out a type.
currentUser: User
}
type Query {
# I will always return a user, but this is not the default.
# Note how you had to add the bang (!).
currentUser: User!
}

At first this may seem strange given that most modern typed languages are the opposite. If you look at typed languages like Flow or Rust you’ll notice that all types are non-null by default. The developer must explicitly tell the type checker when a type is optional. For instance, to define a nullable string in Flow you would write: ?string, in Rust: Option<String>, and in Haskell: Maybe String.

Why would this convention be flipped in GraphQL? Unlike these modern typed languages GraphQL aims to serve a variety of different clients from a single schema. When you write a type in Flow you control all of the consumers for that type. However, in GraphQL you may have many teams depending on the same API, and more importantly your typing decisions are hard to reverse given the popular “versionless API” nature of GraphQL.

In addition, nullable fields make sense as the default given some of the costs of non-null fields. The first of these costs is that…

Non-null fields make it hard to evolve your schema

If you are looking to add a new feature to your application and you need to change your schema, you can always make a field non-null if it was nullable at first. This will not break your GraphQL clients because they were always expecting null. If the client never gets null then the code to handle nulls will never run. However, you can never make a non-null field nullable. The code in your GraphQL clients will break the first time they encounter a null because they didn’t know it was possible that a null could be returned from a given field.

type User = {
// Generated from the GraphQL schema: `age: Int`.
// In your code Flow will make sure that you handle
// the case where `age` may be null.
age: number | null,
};
type User = {
// Generated from the GraphQL schema: `age: Int!`.
// In your code you probably won’t handle the case where
// `age` is null. If you ever remove the requirement to
// specify an age on users your code will break because it
// doesn’t expect nulls!
age: number,
};

This makes it much harder for you to evolve your schema in the future. When designing your schema its important to ask yourself: “is there any plausible universe no matter how improbable where I may want this to be null?” If the answer is yes (or if the question is too complex to answer) then perhaps consider sticking with the GraphQL default and make the field nullable. You can always make your fields non-null if you change your mind.

Some of the future cases where you might regret making a field non-null include:

  • You add a new class of values returned by the given type. For example, you may require a headline on a post (headline: String!), but in the future you may allow posts without headlines. Using just a description to entice readers. (In RSS, for instance, <title> elements for RSS <item>s are optional.)
  • You allow a user to hide the value of a field for privacy purposes. If a field is non-null then you must return some opaque value. For example, if you defined age as a non-null integer you may want to start returning -1 if you can’t return null.
  • You deprecate a field, but because a field is non-null you must always provide a value for this field. Even for new instances of the type.
  • You introduce a new backend service that will deliver values for a given field, but this backend service fails and has legitimate errors sometimes. If your field is non-null then the error will propagate up your response and you will lose the ability to handle the error for just that field in your UI. More on this in the next section…

Non-null fields mean small failures have an outsized impact

This is a less obvious downside of non-null fields, but one you should consider nonetheless. Whenever an error happens in a non-null GraphQL field then that error is propagated up to the first nullable field. This means that small errors that may be constrained to a single field end up wiping out what may have been useful data. Here’s how this would work in a real world application:

# Schema
schema {
query: Query
}
type Query {
users: [User]
}
type User {
id: Int!
name: String
imageURL: String
}
# Query
query {
users {
id
name
imageURL
}
}

When everything is running fine this query will return:

{
"data": {
"users": [
{
"id": 1,
"name": "Sara Smith",
"imageURL": "https://example.org/image-1.jpeg"
},
{
"id": 2,
"name": "John Smith",
"imageURL": "https://example.org/image-2.jpeg"
},
{
"id": 1,
"name": "Budd Deey",
"imageURL": "https://example.org/image-3.jpeg"
}
]
}
}

Now let’s say our image service is having a bad day and can’t return an image for the second user. With a nullable field the response will look like:

{
"data": {
"users": [
{
"id": 1,
"name": "Sara Smith",
"imageURL": "https://example.org/image-1.jpeg"
},
{
"id": 2,
"name": "John Smith",
"imageURL": null
},
{
"id": 1,
"name": "Budd Deey",
"imageURL": "https://example.org/image-3.jpeg"
}
]
},
"errors": [
{
"message": "Image server failed to return the user’s image",
"path": ["users", 1, "imageURL"]
}
]
}

Here we have a descriptive error in the exact location that the error occurred. However, what if imageURL was a non-null string? Defined with imageURL: String!. Then the error would propagate up and swallow all of the data for our second user. We may have been able to use that data to render the user partially, but now we can’t.

{
"data": {
"users": [
{
"id": 1,
"name": "Sara Smith",
"imageURL": "https://example.org/image-1.jpeg"
},
null,
{
"id": 1,
"name": "Budd Deey",
"imageURL": "https://example.org/image-3.jpeg"
}
]
},
"errors": [
{
"message": "Image server failed to return the user’s image",
"path": ["users", 1, "imageURL"]
}
]
}

If we were using an intelligent GraphQL client then the imageURL could have then been updated automatically from another query, but now that we lost all the data from our second user that will not happen.

However, what if we really love non-null types and we decided that not only did we want imageURL to be non-null, but we also want to make sure that all of the users in our users array were also non-null. So we define the query level users field as: users: [User!]. This seems harmless, but watch what happens when the null propagates.

{
"data": {
"users": null
},
"errors": [
{
"message": "Image server failed to return the user’s image",
"path": ["users", 1, "imageURL"]
}
]
}

The entire array of users is wiped out. Instead of handling the error in the one place it occurred the one error propagated through our schema which means we now have no data to render.

You’ll notice in this example that there was one non-null field. id! Generally it makes sense to make id non-null because when id is null you’ll typically want this error propagation behavior.

Conclusion

If the benefit of non-null types is great enough for your UI development then perhaps consider a GraphQL query transformer like GraphQL Lodash where a UI developer can mark a field that they want as non-null locally instead of globally at a GraphQL schema level. Perhaps with a directive like: @nonNull. This allows you to keep the semantics of a nullable field for all clients while allowing single product developers to make the non-null decision for themselves.

Though do remember, while non-nulls in your GraphQL schema are a very powerful tool they do have some unforeseen costs. If you are considering creating a non-null field you should ask yourself: “Is there ever on any platform at any point in time ever a plausible scenario where I might want to render this type anyway even if this field is null?” If the answer is yes then the costs of non-null type may outweigh the benefits. Of course, in some cases the benefits of the non-null invariant (like in the case of id fields) is powerful enough that it makes sense to go non-null anyway. That decision is up to you and your API consumers. Otherwise, if the question is too complex to answer in a short period of time and not that important then choosing a nullable field is a good default.