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.

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

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.

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!
}

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,
};
  • 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
}
}
{
"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"
}
]
}
}
{
"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"]
}
]
}
{
"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"]
}
]
}
{
"data": {
"users": null
},
"errors": [
{
"message": "Image server failed to return the user’s image",
"path": ["users", 1, "imageURL"]
}
]
}

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.

Product engineer at Airtable. Previously Facebook. @calebmer on Twitter