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
- 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.
- 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.
- 03Note the request context: authentication headers, user role, tenant, and any feature flags active during the failing call.
- 04Check server access logs for the request identifier and confirm whether the resolver function was actually invoked, returned early, or threw.
- 05Reproduce with a minimal query that selects only the suspect field, so middleware, variables, and sibling resolvers are isolated from the failure.
- 06Compare responses from a privileged context and a restricted context using the exact same query to surface authorization-driven differences.
- 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
- 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.
- 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.
- 03Re-issue the original query with a privileged context; if the null disappears, the cause is authorization masking rather than resolver or data logic.
- 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.
- 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.
- 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.
- 07Validate that the response parser is not collapsing errors during JSON parsing by comparing the raw response bytes to the parsed object tree.
- 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
- 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>