RFC 0 / Strawman

True Nullability Schema

Opened on2023-09-12
Updated on2025-02-12

At a glance

WG discussion

*TL;DR: If GraphQL offered an option to opt-out of server null bubbling, GraphQL clients that are capable of handling field errors client-side could safely expose the true nullability of fields to product code.

This post outlines a direction we are discussing internally at Meta. Our goal in sharing is to gauge community interest and solicit feedback before we pursue proposing any specific RFCs.

The Problem

Today it is considered a best practice to type schema fields as nullable by default in order to allow the GraphQL server to return null for fields who’s resolvers throw during execution. This behavior ensures response resiliency by containing the blast radius (destructive impact) of any individual field error while still ensuring the data portion of the response is always type-safe with regards to the schema.

However, this pervasive schema nullability comes at a cost. In product code, ~every field must be null-checked before use. Unfortunately, in practice this is impractical. While some null values can be handled by client code, many null values cannot, or are simply not worth the effort. The result is that many potential null values are dealt with via destructive assertions. See @required and Client Controlled Nullability’s (CCN) !, as two examples of this approach.

What’s worse, the true/expected nullability of a field has been lost. Client developers are adding these assertions without knowledge of which fields are actually expected to return null, and which only do so in exceptional cases.

Letting Smart Clients Handle Errors

As stated above, the goal of coalescing errors to null is to allow the response data to always be type-safe with regard to the schema. But that’s really just a means to an end. Our true goal is to ensure product code only sees type-safe data. What if a smart client could ask the server not to perform null bubbling, and instead take on this responsibility itself?

With null bubbling disabled, even non-nullable fields might be missing in the data portion of the response, rendering it no-longer type-safe. But, the data portion minus the fields mentioned in the errors metadata would still be type-safe.

A sufficiently smart client could parse the errors metadata of the response, and ensure that reading any GraphQL data that includes a field error results in an error. This is especially attractive for clients that encourage data colocation, where data is exposed to product code at a fragment granularity. This allows the blast radius of a field error to be limited to the fragment/component in which it was read.

This is a project we are actively exploring in Relay this year. For even more resiliency, we’re exploring a @catch directive which catches field errors and transforms them into a result types.

True Schema Nullability

With the client taking responsibility for shielding product code from field errors, we no-longer need every field in the schema to be nullable just to absorb errors. Instead, every field in the schema can reflect the true nullability of its backing resolver.

Product code no-longer needs to null-check fields which are actually non-nullable, and fields which are typed as nullable are actually expected to be null, and thus clients should be expected to handle those nulls gracefully. This leaves no need for destructive assertion features such as @required and CCN’s !.

Disabling Null Bubbling

By sheer coincidence, CCN’s ? would actually allow compiler-based smart clients to opt out of null bubbling by annotating every field with a ?. I’ve described that approach here. But if this approach to GraphQL looks attractive to the broader community, it might be worth considering a more explicit mechanism to enable this behavior.


Appendix

This approach is not without challenges or tradeoffs. I’ll try to capture the ones of which I’m aware here:

Error boundaries

This approach is dependent upon having a client architecture that allows product code to contain errors thrown during render. Relay’s insistence on data colocation combined with React Error Boundaries provide this resilience, but client architectures that read a full query at a time, or don’t have a mechanism for containing errors thrown during render, may not be able to handle errors while still remaining robust.

Breaking changes

Another reason that GraphQL recommends that all fields be nullable, even if their current implementation is non-nullable, is that it allows us to turn a non-nullable field into a nullable field as a non-breaking change. This is especially important on mobile where clients live essentially forever. Being able to make a field nullable can be key to being able to delete code.

I don’t have a solution to this problem, but I am curious to learn how well it works in practice. Have users of this approach actually be able to routinely make fields nullable without breaking old clients? Are product engineers really designing apps that gracefully degrade in the face of any field being null? The convergent evolution of @required and CCN’s ! makes me question if this is true, and @martinbonnin seems to agree.

Smart and simple clients sharing a schema

This approach involves a smart client and a collaborating schema that exposes its true nullability. However, if you need to support resiliency with both smart and simple clients this approach alone will not be sufficient. More thought might be required to support this setup.

Related Posts

Timeline