graphql-spec icon indicating copy to clipboard operation
graphql-spec copied to clipboard

Add Transitional Non-Null appendix (`@noPropagate` directive)

Open benjie opened this issue 5 months ago • 0 comments

This is essentially solution 8 to the Semantic Nullability RFC:

  • Enables semantic nullability to be reflected in schemas without breaking legacy behavior.
  • Facilitates incremental adoption of modern error handling without requiring disruptive changes.
  • Requires minimal spec impact and is fully optional for implementations.
  • Reflects transitional nature of this change in behavior

I've based it on:

  • #1163

since, like all solutions to the semantic nullability problem[^1], it is designed to enable clients with error propagation disabled to leverage the true nullability of the underlying data without breaking legacy clients. The approach could be rebuilt atop an alternative method of toggling error propagation, for example a directive-based approach.

[^1]: Except solution 5


This PR introduces an appendix to the GraphQL specification defining an optional solution to the semantic nullability problem using the following key mechanisms:

  • @noPropagate[^2] directive — allows schema authors to annotate Non-Null return types as transitional, suppressing propagation but preserving runtime error generation.
  • Transitional Non-Null semantics — errors at these positions behave like nullable fields in terms of (no!) propagation but like non-nullable fields in value completion (error on null).
  • New __Field.noPropagateLevels: [Int!] field — exposes transitional status to modern clients.
  • Transitional non-null hidden from legacy clients — tooling using the legacy PROPAGATE error behavior will get results from __Field.type that unwrap transitional non-null types.[^3]

[^2]: This is essentially the same as the @semanticNonNull directive, but more strictly defined and reflected through introspection.

This solution attempts to address all of the feedback on previous solutions to this problem, whilst being explicitly transitional. It:

  • Is optional: explicitly only for schemas supporting legacy clients
  • Requires no changes to the main spec text
  • Introduces no new syntax
  • True to its name: an error here will not propagate (@noPropagate), regardless of whether error propagation is enabled or disabled.
  • Maintains introspection results for existing (deployed) clients and tooling
  • Maintains error boundaries for existing (deployed) clients
  • Allows all new schemas and new fields to use ! (non-null) directly for semantically non-null positions
  • Allows existing fields to use ! (non-null) for error handling clients without breaking legacy clients by adding the @noPropagate directive[^4]
  • Can be adopted gradually, field-by-field, or en masse by applying @noPropagate to all nullable positions.
  • Can be removed from each field the moment no legacy clients query it

[^3]: This may be controversial, but I truly think it's the right decision. All new tooling (and all new clients!) should use onError: ABORT or onError: NO_PROPAGATE, and thus will see the true introspection. Existing tooling doesn't know about onError and so should not see these "transitional" non-null types. [^4]: And if you forget to add it, adding it later is only a potentially breaking change for any new versions of legacy clients deployed since the change; error-handling clients (NO_PROPAGATE or ABORT) are unimpacted.

benjie avatar Apr 30 '25 16:04 benjie