Nullability in GraphQL.js
Nullability is a core concept in GraphQL that affects how schemas are defined, how execution behaves, and how clients interpret results. In GraphQL.js, nullability plays a critical role in both schema construction and runtime behavior.
This guide explains how nullability works, how it’s represented in GraphQL.js, and how to design schemas with nullability in mind.
How nullability works
In GraphQL, fields are nullable by default. This means if a resolver function
returns null
, the result will include a null
value unless the field is
explicitly marked as non-nullable.
When a non-nullable field resolves to null
, the GraphQL execution engine
raises a runtime error and attempts to recover by replacing the nearest
nullable parent field with null
. This behavior is known formally as “error
propagation” but more commonly as null bubbling.
Understanding nullability requires familiarity with the GraphQL type system, execution semantics, and the trade-offs involved in schema design.
The role of GraphQLNonNull
GraphQL.js represents non-nullability using the GraphQLNonNull
wrapper type.
By default, all fields are nullable:
import {
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
} from 'graphql';
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLString) },
email: { type: GraphQLString },
}),
});
In this example, the id
field is non-nullable, meaning it must always
resolve to a string. The email
field is nullable.
You can use GraphQLNonNull
with:
- Field types
- Argument types
- Input object field types
You can also combine it with other types to create more specific constraints. For example:
new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(UserType)))
This structure corresponds to [User!]! in SDL: a non-nullable list of non-null
User
values. When reading code like this, work from the inside out: UserType
is non-nullable, and wrapped in a list, which is itself non-nullable.
Execution behavior
GraphQL.js uses nullability rules to determine how to handle null
values
at runtime:
- If a nullable field returns
null
, the result includes that field with anull
value. - If a non-nullable field returns
null
, GraphQL throws an error and sets the nearest nullable parent field tonull
.
This bubbling behavior prevents partial data from being returned in cases where a non-nullable guarantee is violated.
Here’s an example that shows this in action:
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
} from 'graphql';
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLString) },
},
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
resolve: () => ({ id: null }),
},
},
});
const schema = new GraphQLSchema({ query: QueryType });
In this example, the user
field returns an object with id: null
.
Because id
is non-nullable, GraphQL can’t return user.id
, and instead
nullifies the user
field entirely. An error describing the violation is
added to the errors
array in the response.
Schema design considerations
Using non-null types communicates clear expectations to clients, but it’s
also less forgiving. When deciding whether to use GraphQLNonNull
, keep
the following in mind:
- Use non-null types when a value is always expected. This reflects intent and reduces ambiguity for clients.
- Avoid aggressive use of non-null types in early schema versions. It limits your ability to evolve the API later.
- Be cautious of error bubbling. A
null
return from a deeply nested non-nullable field can affect large portions of the response.
Versioning
Non-null constraints are part of a field’s contract:
- Changing an output position (field type) from non-nullable to nullable is a
breaking change - clients may now receive
null
values which they do not have handling code for. - Changing an input position (argument or input field type) from nullable to non-nullable is a breaking change - clients are now required to provide this value, which they may not have been supplying previously.
- Changing an output position from nullable to non-nullable will not break deployed clients since their null handling code will simply not be exercised.
To reduce the risk of versioning issues, start with nullable fields and add constraints as your schema matures.
Using GraphQLNonNull
in schema and resolvers
Let’s walk through two practical scenarios that show how GraphQL.js enforces nullability.
Defining a non-null field
This example defines a Product
type with a non-nullable name
field:
import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql';
const ProductType = new GraphQLObjectType({
name: 'Product',
fields: () => ({
name: { type: new GraphQLNonNull(GraphQLString) },
}),
});
This configuration guarantees that name
must always be a string
and never null
. If a resolver returns null
for this field, an
error will be thrown.
Resolver returns null
for a non-null field
In this example, the resolver returns an object with name: null
, violating
the non-null constraint:
import {
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
GraphQLSchema,
} from 'graphql';
const ProductType = new GraphQLObjectType({
name: 'Product',
fields: {
name: { type: new GraphQLNonNull(GraphQLString) },
},
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
product: {
type: ProductType,
resolve: () => ({ name: null }),
},
},
});
const schema = new GraphQLSchema({ query: QueryType });
In this example, the product
resolver returns an object with name: null
.
Because the name
field is non-nullable, GraphQL.js responds by
nullifying the entire product
field and appending a
corresponding error to the response.
Best practices
- Default to nullable. Start with nullable fields and introduce non-null constraints when data consistency is guaranteed.
- Express intent. Use non-null when a field must always be present for logical correctness.
- Validate early. Add checks in resolvers to prevent returning
null
for non-null fields. - Consider error boundaries. Were an error to occur, where should it stop bubbling?
- Watch for nesting. Distinguish between:
[User!]
- nullable list of non-null users[User]!
- non-null list of nullable users[User!]!
- non-null list of non-null users
Additional resources
- GraphQL Specification: Non-null: Defines the formal behavior of non-null types in the GraphQL type system and execution engine.
- Understanding GraphQL.js Errors: Explains how GraphQL.js propagates and formats execution-time errors.
- Anatomy of a Resolver: Breaks down how resolvers work in GraphQL.js.
- Constructing Types: Shows how to define and compose types in GraphQL.js.