GraphQL · advanced

GraphQL null-field checklist

This playbook gives advanced GraphQL engineers an operational checklist for diagnosing fields that return null unexpectedly inside a partial response, rather than as part of a top-level errors array. It focuses on the contract defined by the GraphQL October 2021 specification around null propagation and field-level error handling, and translates it into decision points a debugger can follow without guessing.

The symptoms

  • A GraphQL response returns HTTP 200 with a populated data object, but one or more leaf fields inside that object are null while sibling fields are populated normally.
  • The same field resolves to a non-null value when the same selection is sent in isolation, but resolves to null when queried as part of a larger document with aliases or fragments.
  • Client code receives a TypeError, KeyError, or NPE when accessing a nested field, but no entries appear in the top-level errors array of the response.
  • Resolver logs show a thrown exception, yet the JSON response still contains partial data shaped exactly as the selection set requested.
  • Telemetry shows a spike in non-null contract violations or in fields whose declared type is non-null but whose runtime value is null, contradicting the schema.

Likely causes

  • A resolver throws or rejects for that specific argument combination, but the error is converted into a data-level null rather than a propagated GraphQL error entry.
  • An upstream service or data store returns null, empty, or a transport error for that branch only, while other branches succeed.
  • An authentication, authorization, or scope check rejects the request for the null branch, and the implementation suppresses the failure into a null instead of raising a typed error.
  • A field whose declared type is non-null is being coerced to null by client-side schema mismatches, generated code drift, or persisted query hash collisions.
  • A NullValue is being injected by a middleware, directive, or interceptor that runs after the resolver but before serialization, masking the underlying failure.
  • Caching layers return a cached null placeholder when the upstream is degraded, hiding transient outages behind a stable null response.

First ten minutes

  1. 01Capture the exact request body, operation name, variables, and headers; record the byte-stable response body and the HTTP status to preserve the partial-null evidence.
  2. 02Diff the response against the schema for the operation: walk every null leaf and confirm whether its declared type is nullable or non-null, since null propagation rules differ.
  3. 03Identify whether the top-level errors array is empty; an empty errors array alongside data-level nulls is the canonical signal that nulls are coming from resolvers, not from spec-level error formatting.
  4. 04Reproduce with the smallest possible selection set that still touches the null path; record variables that trigger the null versus variables that return data.
  5. 05Correlate the failing request with server logs at the request, resolver, and downstream call boundaries to locate where the value is lost.
  6. 06Decide whether the null is contractually valid (nullable field, no data exists) or contractually invalid (non-null field forced to null, or silent failure of a side effect) before changing code.

Evidence to collect

  • The full request document including operation name, variables, and headers, plus the byte-stable JSON response containing both data and errors.
  • The schema definition for the operation, including the nullability of every field on the path from root to the null leaf.
  • Resolver and middleware logs bracketing the failing request id, including thrown exceptions, returned values, and any thrown vs returned errors.
  • Downstream service traces for the same request id, including timeouts, retries, and circuit-breaker states at the moment of the null.
  • Cache key, cache hit or miss, and stored payload if a caching layer sits between the client and the resolver; cached nulls are a frequent cause of repeated silent nulls.
  • Recent deployment, schema change, or directive change notes that could have altered how errors are converted into null for that field.

Where to look

  • At the resolver boundary, inspect the function mapped to the null field and any wrapping middleware that can swallow exceptions into null.
  • At the data access boundary, inspect the database, search index, or downstream service call that supplies the null field, including its timeout, retry, and fallback policy.
  • At the authorization boundary, inspect any scope, role, or policy check that runs before resolution and may short-circuit to null instead of raising.
  • At the schema boundary, inspect the SDL for the type and field in question to confirm declared nullability versus the runtime contract you assumed.
  • At the transport boundary, inspect any persisted query, query plan, or batched request layer that could rewrite the selection and silently drop fields.
  • At the caching boundary, inspect any response, entity, or field cache that could be returning a stored null placeholder after a transient failure.

Diagnostic steps

  1. 01Reproduce the null against a known-good client and a minimal selection set containing only the suspect field; if it returns null in isolation, the cause is in the resolver chain, not in field interaction.
  2. 02Compare the response when the same field is requested with different aliases or fragments; differences indicate resolver argument handling or per-alias authorization state.
  3. 03Enable verbose resolver tracing for the operation name and confirm whether the resolver for the null field was actually invoked, returned undefined, threw, or was short-circuited by middleware.
  4. 04Inject a temporary resolver wrapper that records the arguments, the resolved value, and any thrown error; this isolates whether the null originates inside the resolver or downstream.
  5. 05Inspect the schema declaration of the field; if the field type is non-null yet the response contains null, the implementation is violating the spec contract and must be treated as a bug, not as a null result.
  6. 06Cross-reference any recent change to error formatting, error masking, or null-on-error policies in middleware, and revert or guard those changes behind a feature flag to confirm causation.

Common mistakes

  • Treating data-level nulls as harmless because the HTTP status is 200 and errors is empty, when the spec still requires non-null fields to surface errors through the errors array or to null up to the nearest nullable parent.
  • Assuming a null field means there is no data, when the resolver is actually throwing and the error is being swallowed by a generic catch that returns null.
  • Adding defensive client-side null checks that mask the symptom, instead of investigating why a field whose contract promises data is silently empty.
  • Confusing schema nullability with runtime nullability, for example treating a non-null Int as if it could legally be null because the client code already handles nulls.
  • Caching a null response without tagging it as a partial failure, which then propagates the silent null across users and obscures the underlying outage.
  • Patching the resolver to coerce errors into nulls for convenience, which converts recoverable failures into silent data loss and breaks the GraphQL error contract.

Safe fixes

  • When evidence shows the resolver throws and the field is nullable, allow the error to propagate into the errors array per the GraphQL spec, and return a typed error instead of a coerced null.
  • When evidence shows the upstream returns null legitimately, leave the field nullable in the schema if it is already nullable, and document the contract so clients handle the empty case explicitly.
  • When evidence shows authorization is rejecting the field, return a typed authorization error in the errors array rather than silently nulling the field, unless the field is explicitly declared nullable for that reason.
  • When evidence shows a cache returned a stored null after a transient failure, mark the cached entry as partial, set a short TTL, and force a refetch on the next request to validate recovery.
  • When the schema declares the field as non-null but the response contains null, change the schema to nullable if the value can genuinely be absent, or fix the resolver so it never returns null for a non-null field.
  • When middleware converts thrown errors into nulls, scope that behavior to explicitly opted-in fields via a directive or configuration, and audit existing usages to confirm the conversion is intentional.

Prove the fix

  1. 01Re-run the original request that produced the null and confirm that either the field now returns the expected non-null value, or the response errors array contains a typed error keyed to the same path, matching the schema declaration.
  2. 02Run a schema-conformance check that sends a request where every field in the selection set is declared non-null and asserts that no field in data is null while errors is empty; the response must conform.
  3. 03Re-run the minimal isolation query and confirm behavior is unchanged between isolation and the full document, ruling out alias, fragment, or batching artifacts as the cause.
  4. 04Inspect cache entries for the failing operation and confirm no null placeholder is being served without a partial-failure tag; force a refetch and verify the fresh value is correct.
  5. 05Capture the request id, response body, and resolver log line for the fixed path and store them as a regression artifact so the same null pattern can be detected in future test runs.

Prevention and next steps

  • Adopt a schema-first review that requires every field to declare explicit nullability and forbids runtime coercion of non-null fields to null without a matching errors entry.
  • Instrument resolvers to emit metrics for null returns, distinguishing contractually nullable nulls from coerced nulls caused by swallowed exceptions, and alert on the latter.
  • Standardize an error-formatting layer that converts resolver exceptions into typed GraphQL errors in the errors array rather than into silent nulls, except for fields explicitly marked nullable-for-resilience.
  • Tag cached null responses as partial-failure entries with short TTLs and a forced refetch policy, so transient upstream failures cannot mask outages behind stable nulls.
  • Add contract tests that assert the nullability of every field in a selection set against the response, and fail the build if a non-null field returns null without a matching error entry.

Safe commands and checks

echo "Capture request: operationName, variables, headers, and the byte-stable response body; store alongside the schema definition for the operation."
echo "Diff the response JSON against the schema; for every null leaf, record the declared type and whether the field is nullable or non-null."
echo "If the field is declared non-null and the response contains null, classify the finding as a spec contract violation and escalate; do not patch with a null check."
echo "Enable verbose resolver tracing scoped to the operation name and request id; verify whether the resolver was invoked, returned undefined, threw, or was short-circuited."
echo "Cross-reference middleware, directive, and error-masking changes deployed in the window between the last green run and the first observed null; revert behind a feature flag to confirm causation."
echo "Add a contract test that asserts the nullability of every selected field against the response, and that fails when a non-null field returns null without a matching errors entry."