GraphQL · intermediate

GraphQL request returns null without an obvious error

GraphQL responses can return null for a field while leaving the errors array empty or empty-looking, leaving engineers with partial data and no obvious failure signal. This guide explains how to distinguish a resolver returning null, an authorization rule masking data, and spec-level null bubbling, then walks through evidence collection, isolation, and verification.

The symptoms

  • A specific field in the JSON response is null while sibling fields on the same object are populated correctly.
  • The HTTP status is 200 OK and the errors array is empty or only contains entries that seem unrelated to the suspect field.
  • Different callers, roles, or tokens see different fields collapse to null with no change to query or schema.
  • A parent object is null even though the query itself completed, suggesting a non-nullable violation upstream rather than a resolver returning null.
  • Server logs show the resolver executed and returned a value, yet the wire response still shows null at the corresponding path.
  • Removing the authentication header or switching to an admin context suddenly restores the missing values.

Likely causes

  • The resolver returns null deliberately, for example when an entity is missing, soft-deleted, or filtered by a conditional branch without throwing.
  • Authorization middleware silently filters fields when the caller lacks permission and substitutes null instead of raising a forbidden error.
  • A non-nullable field receives null and the null bubbles up through parent paths, leaving only a top-level or sibling error that does not name the originating field directly.
  • A DataLoader or batch loader returns null for a missing key without surfacing an upstream not-found error to the caller.
  • A cache layer returns a stored null or a no-op miss that propagates as data, with the underlying resolver never executed.
  • The ORM or database query returns zero rows and the resolver coerces that result to null without distinguishing missing-data from forbidden-data.

First ten minutes

  1. 01Capture the full HTTP response body verbatim, including the errors and extensions arrays, and print data and errors side by side rather than reading only the parsed object.
  2. 02Identify whether the suspect field is declared as nullable or non-nullable in the SDL or schema registry, since the spec treats the two cases differently.
  3. 03Note the request context: authentication headers, user role, tenant, and any feature flags active during the failing call.
  4. 04Check server access logs for the request identifier and confirm whether the resolver function was actually invoked, returned early, or threw.
  5. 05Reproduce with a minimal query that selects only the suspect field, so middleware, variables, and sibling resolvers are isolated from the failure.
  6. 06Compare responses from a privileged context and a restricted context using the exact same query to surface authorization-driven differences.
  7. 07Diff the response with and without any client-side caching, normalization, or persisted-query layer that may strip or rewrite fields.

Evidence to collect

  • The complete JSON response body saved verbatim, with errors, extensions, and the exact JSON path of every null field annotated.
  • The relevant portion of the SDL showing the parent type, the suspect field, its declared nullability, and any directives such as @auth or @required applied to it.
  • The resolver source for the suspect field, including any wrapper functions for authentication, caching, logging, or error transformation.
  • The authorization rules or policies attached to the field or its parent type, including rule order and fallback behaviour.
  • Server logs filtered by the request identifier, including thrown exceptions, rejected promises, and any debug-level resolver traces.
  • Network timing and cache headers indicating whether the response was served from cache or hit the resolver path on the failing request.

Where to look

  • In the response JSON, the exact path of the null field, paired with any sibling entries in errors whose path or extensions reference that location.
  • In the SDL or schema registry, the field declaration, its nullability marker, and any directives that influence visibility or requiredness.
  • In the resolver file, the function body, early returns, conditional branches, and any explicit null literals introduced for fallback logic.
  • In the authorization layer, the rules applied to the field or type, the deny behaviour, and whether the layer throws or coerces to null on failure.
  • In the data access layer, the query that backs the resolver, including empty result handling and how zero rows are mapped to a return value.
  • In the cache layer, the key namespace, TTL behaviour, and any null-caching policy that may have stored a prior empty response.

Diagnostic steps

  1. 01Run an introspection query for the suspect type to confirm whether the field is declared nullable; if it is non-nullable, treat the null as a possible upstream bubble rather than a deliberate return.
  2. 02Add temporary resolver-level logging that records the input arguments, any thrown error, and the return value before any wrapper or formatter transforms it, gated by a debug flag.
  3. 03Re-issue the original query with a privileged context; if the null disappears, the cause is authorization masking rather than resolver or data logic.
  4. 04Call the underlying data source directly with the same arguments to verify whether the backing record exists, was deleted, or is filtered at the storage layer.
  5. 05Disable or bypass the cache layer using a cache-control header or a dedicated bypass flag, and compare the response to isolate cache-induced nulls.
  6. 06Inspect the errors array for entries with empty messages but populated extensions, since some clients hide errors that lack a human-readable message by default.
  7. 07Validate that the response parser is not collapsing errors during JSON parsing by comparing the raw response bytes to the parsed object tree.
  8. 08Trace the call from the gateway through any field-level plugins or transforms that may rewrite the value before it reaches the wire response.

Common mistakes

  • Reading only response.data and ignoring response.errors, so a populated errors array explains the null but is never observed.
  • Assuming a non-nullable type guarantees a non-null response on the wire and missing the null-bubbling behaviour described in the GraphQL specification.
  • Concluding the resolver is broken when authorization middleware actually filters or rewrites the value before the resolver body runs.
  • Treating HTTP 200 OK as proof of success even though the specification permits partial responses that still include errors.
  • Forgetting that some client libraries drop fields not present in the local client schema, masking the original null source in the wire response.
  • Adding null-coalescing or fallback logic in resolvers to silence errors, which then surface as silent nulls instead of actionable failure messages.

Safe fixes

  • Change resolvers that return null on missing data to throw a typed error such as NotFoundError, so the errors array surfaces the cause with a field path.
  • Configure authorization middleware to throw a ForbiddenError rather than substituting null, ensuring callers see a clear and consistent failure signal.
  • Add a server-side error formatter that includes the field path and an error code on every entry, so silent nulls can be traced back to the originating field.
  • Tighten SDL declarations so that fields are nullable only when absence is semantically meaningful, and mark truly required fields as non-nullable.
  • Introduce a debug mode in non-production environments that wraps each resolver with before and after logging, gated by an environment flag to avoid leaking into production.
  • Add schema tests that assert non-null fields never resolve to null across all defined contexts and roles, catching silent masking before deployment.

Prove the fix

  1. 01Schema review notes show that the field's declared nullability and the resolver contract are aligned and reviewed together.

Prevention and next steps

  • Adopt a resolver convention: throw typed errors for missing data and reserve null only for genuine "not applicable" semantics that are documented in the schema.
  • Define the SDL with intentional nullability and review every non-nullable field in code review against its resolver to keep declarations and behaviour consistent.
  • Centralize authorization in a single layer that throws on denial rather than silently filtering, and unit-test that layer against expected deny paths.
  • Enable error logging in production with sensitive data masked, so silent nulls remain traceable through request identifiers and error codes.
  • Maintain a contract test suite that exercises each field across roles and asserts either a value or a populated error, never a silent null on a non-nullable field.

Safe commands and checks

curl -sS -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer <token>' -d '{"query":"{ user(id:\"<id>\") { id email posts { title } } }"}' <endpoint>
curl -sS -X POST -H 'Content-Type: application/json' -d '{"query":"{ __type(name:\"User\") { fields { name type { kind name ofType { kind name } } } } }"}' <endpoint>
curl -sS -X POST -H 'Cache-Control: no-cache' -H 'Authorization: Bearer <token>' -d '<query-payload>' <endpoint>
curl -sS -X POST -H 'Authorization: Bearer <admin-token>' -d '<query-payload>' <endpoint>
curl -sS -X POST -H 'Authorization: Bearer <user-token>' -d '<query-payload>' <endpoint>