skip to content

In a GraphQL response, how do you tell a legitimately null field from one nulled by an error?

level: middleimportance: should knowfreq 48%

answer

  1. One null on the wire, two causes
  2. The errors list is the only discriminator
  3. Compare the hole's position against error paths
  4. A prefix match, not an exact match
  5. No matching path means a real null

basics

~20 s

Check the errors list. A null a resolver deliberately returned has no error pointing into it; a null caused by a failure has an errors entry whose path is that position or runs deeper through it.

solid answer

~50 s

In `data` both cases are the literal `null` — the wire format has no flag separating "no value" from "this broke". You discriminate by cross-referencing the errors list: take each entry's `path`, and treat a null at response position P as failure-caused if some error path equals P **or starts with P**. The prefix case matters because a failure on a Non-Null field surfaces the null at the nearest nullable ancestor, so the error names a position at or below the hole you are inspecting. If no error path lands inside that subtree, the null is a value the resolver returned — an unassigned advisor, a course with no waitlist — and it should render as an empty state, not an error. A field the document never selected is a third case again: it is simply absent from the map.

code

json · 15 lines
json
{
  "data": {
    "enrollment": {
      "id": "ENR-4471",
      "student": { "name": "R. Ibarra", "advisor": null },
      "section": { "code": "STAT-101-B", "instructor": null }
    }
  },
  "errors": [
    {
      "message": "Instructor directory unavailable",
      "path": ["enrollment", "section", "instructor", "name"]
    }
  ]
}

go deeper

for a junior

Recall that a null in the response can be an ordinary value, not necessarily a failure, and that the errors list is where you look to tell. Do not render an error state over every null you meet.

for a middle

Be able to state and apply the rule: a null is failure-caused when some error path starts with that position. Explain why a Non-Null failure puts the null above the errored field, and mention the entries that carry no path at all.

for a senior

Demonstrate that you have built this attribution once at the boundary rather than scattering null checks through the view layer, and that you keep absent, real-null and error-hole as three distinct outcomes with three distinct renders.

for a principal

Frame it as a schema contract question: which fields are allowed to be null for ordinary reasons, and whether the API leans on nullable payload fields or an explicit result shape so clients need less inference. That choice sets how much of this logic every client has to write.

### One representation, two very different causes In a GraphQL response body, a missing value is written as JSON `null`, and there is exactly one `null`. It is used both for a value a resolver deliberately returned — a course with no assigned instructor yet, a student with no advisor — and for a hole the execution engine punched where a field error occurred. Nothing in `data` itself tells the two apart: there is no marker byte, no sentinel object, no flag on the parent. A client that wants to render "not set yet" differently from "we could not load this" has to reconstruct the distinction, and the only material available is the top-level `errors` list. ### The rule: prefix, not equality Each errors entry carries a `path` whenever the failure can be attributed to a field in the result — the sequence of response keys from the root of `data` down to the field that raised the error, with 0-indexed integers for list positions. The naive correlation is to look for an error whose path is exactly the position of the null you are inspecting. That is right only in the simplest case. When the field that failed is declared Non-Null, the engine cannot write `null` into that position, so the null appears **higher up**, at the nearest nullable ancestor, and everything below that ancestor — including sibling fields that had resolved perfectly well — is gone from the response. The error's path still names the deep field that actually broke. So the null you are staring at sits at an *ancestor* of the error path. That gives the reading rule a client should implement: > A `null` at response position `P` is failure-caused if some error entry's `path` **starts with** > `P` (which includes the case where the path equals `P`). If no error path starts with `P`, that > null is a value the resolver returned. Entries that carry no `path` at all are a separate bucket: they are not attributable to a position in the tree, so they can never explain a specific null and should be surfaced as a whole-operation problem rather than pinned onto a widget. ### A worked example An enrolment client requests one enrolment with its student and its section: ```json { "data": { "enrollment": { "id": "ENR-4471", "student": { "name": "R. Ibarra", "advisor": null }, "section": { "code": "STAT-101-B", "instructor": null } } }, "errors": [ { "message": "Instructor directory unavailable", "path": ["enrollment", "section", "instructor", "name"] } ] } ``` Two nulls, two different meanings. `["enrollment", "student", "advisor"]` is not a prefix of the single error path, so it is a real null — this student has no advisor assigned. The UI should render an empty state, an "assign advisor" affordance, whatever the product says an unassigned advisor looks like. Showing an error there is wrong and will generate support tickets about a system that is working. `["enrollment", "section", "instructor"]` **is** a prefix of the error path, so that null is a hole: the instructor's `name` is Non-Null, its resolver failed, and the null propagated up to the nullable `instructor` field, taking the rest of the instructor object with it. The UI should render "instructor unavailable", and the client should log the path. ### Implementing the check ``` function isErrorHole(nullPath, errors): for e in errors: if e.path is absent: continue # not attributable to a position if length(e.path) < length(nullPath): continue if e.path[0 .. length(nullPath) - 1] == nullPath: return true return false ``` Two details matter in practice. First, path elements are heterogeneous — strings for response keys, integers for list indices — so compare element-wise rather than joining them into a dotted string, where the key `"1"` and the index `1` would collide. Second, the response key is the **alias** when the document used one, so the path follows the shape of the response the client asked for, not the schema's field names. ### The third case: absent A key that was never selected is not present in the response map at all. In a dynamically typed client, reading it yields undefined/missing rather than `null`, and conflating that with a null is a common bug — it usually shows up when a fragment is conditionally included and the code treats "we did not ask" as "there is nothing there". Absent, null, and null-with-an-error are three distinct outcomes and deserve three distinct code paths. ### Why an interviewer asks this It separates candidates who have only read GraphQL responses in a playground from those who have written the boundary layer that turns one into view state. The prefix rule is the compact proof that you understand both what a path points at and what happens to a null when a Non-Null field fails — without which a client either shows error banners over perfectly ordinary empty data, or silently renders holes as if nothing went wrong.

  • Why is a prefix match, rather than an exact match, the right test?
    Because the null does not always appear where the error happened. If the failing field is Non-Null, the engine cannot write null into it, so the null surfaces at the nearest nullable ancestor while the error path still names the deeper field. Matching exactly would classify that ancestor's null as a legitimate value and render an empty state over a genuine failure.
  • An errors entry arrives with no path at all — what does a client do with it?
    Treat it as unattributable. Without a path there is no position in the tree to pin it to, so it cannot explain any particular null and must not be attached to a widget. Surface it at the operation level — a banner, a log line — and keep rendering whatever data contains, since the nulls in the tree still have to be judged on their own.
  • Why compare path elements individually instead of joining them into a dotted string?
    Because a path mixes strings for response keys with integers for list indices. Joining collapses that distinction, so a field literally named "1" and list index 1 become the same token, and a prefix test on the joined string can also match halfway through a key. Comparing element by element, with the types intact, avoids both.

saying these in an interview costs you the question

  • Assumes every null in data came from an error
  • Assumes a null with no error is a bug
  • Matches error paths exactly and misses bubbled nulls
  • Treats an absent key and a null identically
  • Guesses the failing field from the message text

context