True Nullability Schema #1394
Replies: 4 comments 6 replies
-
Very interesting! If I understand correctly:
Is it accurate / did I miss anything? |
Beta Was this translation helpful? Give feedback.
-
What about something like this, where there is an error handling mode, and not just a toggle between bubbling and non-bubbling? @errorHandling(mode:
LEGACY # return null for nearest nullable parent. (current/default behaviour)
| NULLIFY_FIELD # return null for the field, regardless of its nullability in the schema.
| NULLIFY_FRAGMENT # return null for the nearest fragment (requires fragment modularity*).
| NULLIFY_ROOT # return null for the whole query.
| OMIT_FIELD # omit the field from the response.
| OMIT_FRAGMENT # omit the nearest fragment (requires fragment modularity [?]).
| OMIT_ROOT # omit the whole query response.
) Most non-legacy modes would require a "smart client" to handle errors.
|
Beta Was this translation helpful? Give feedback.
-
I have an alternate proposal in #1410 I’d love this group’s thoughts on. I resonate strongly with the problem statement here but am not sure about the proposed solution. The intent of the NonNull In hindsight I regret this design choice. Part of why I’ve been excited about CCN is that it’s clear this “required” state should be driven by the client not the server. So I imagine with error bubbling disabled that schema would be far more likely to add NonNull types everywhere and I think that would be a big mistake. But still I really resonate with the premise:
Using NonNull gets us almost the first, but fails at the second. This proposal attempts to shore up that second, but with collateral cost to other clients not using it like having multiple apps hitting one service, or GraphQL as a public API. If NonNull proliferates to be the common case then a client robustness gets worse if this directive isn’t enabled in operations, and it becomes an unstated requirement to use the server successfully |
Beta Was this translation helpful? Give feedback.
-
I read through the comments here and the CCN proposal. Having a schema which defines nullability separate from the clients capability to handle errors would be very beneficial, and I support the overall concept. I believe using First, the For clients that can handle errors for any field, each field in the request could be annotated with An alternative proposed where As the current Client Controlled Nullability RFC currently contains only the |
Beta Was this translation helpful? Give feedback.
-
*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 thedata
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
Beta Was this translation helpful? Give feedback.
All reactions