In a GraphQL response, why can a selected field's key be absent rather than null?
answer
- Three states, not two
- Nothing failed, so nothing was logged
- The drop happens before any resolver runs
- Directive arguments come from the variables
- A type condition can miss at run time
basics
~20 sBecause the field was never collected. Collection runs before resolution and drops selections excluded by @skip or @include, or sitting in a fragment whose type condition misses the runtime type. An uncollected field gets no response key at all.
solid answer
~40 sA `null` means the field was collected, resolved and produced nothing — or was nulled by an error, which leaves an `errors` entry with a `path`. A missing key means the executor never had that field in its grouped field set: no resolver ran, no error exists, and client code reading the field gets `undefined` rather than `null`. Three collection-time causes account for nearly all of it: `@skip(if:)` or `@include(if:)` evaluated against a variable the client sent the wrong way; a fragment spread or inline fragment whose type condition did not apply to the concrete object type returned; and a response key the client is not actually reading, because the document aliased the field. Diagnose it against the document and its variables, not against the schema.
code
graphql · 10 linesquery PaddockTile($paddockId: ID!, $withMoisture: Boolean!) {
paddock(id: $paddockId) {
name
sensorCount
sensors {
serial
soilMoisture @include(if: $withMoisture)
}
}
}go deeper
Read a response literally: a key that is not there is not the same as a key whose value is null. Check the document you actually sent, and the variables you sent with it, before suspecting the server.
Explain the collection step that discards selections — directive arguments evaluated from variables, and type conditions tested against the runtime object type — and why neither leaves an error behind.
Drive the diagnosis: reproduce with the caller's exact operation and variables, separate absent from null from errored, add __typename, and only then reach for server logs. Then add the logging that would have shown it the first time.
Own the observability contract: operation names and variable shapes recorded per request, clients that handle an absent key as its own case, and a deliberate position on how much conditional selection you want in client documents.
## Three states, not two A GraphQL response can say three different things about a field, and conflating any two of them wastes a debugging session. - **The key is present with a real value.** The field was collected, resolved and completed. - **The key is present with `null`.** Either the resolver legitimately produced nothing, or the field errored and was nulled — in which case there is an entry in `errors` carrying a `path` that points at it. - **The key is absent entirely.** The field was never in the grouped field set. No resolver ran, no value was completed, no error exists, and there is nothing in the response or the server logs that mentions it. Client code feels the difference immediately: an absent key reads as `undefined` in a JavaScript-shaped client, not `null`, and a destructuring read or a strict schema check on the client can fail on it while a `null` sails through. ## Where a field gets dropped Field collection runs over an object's selection set *before* any of that object's fields resolve, and it has exactly two places where a selection is discarded rather than grouped. **A directive said no.** For each selection, collection evaluates `@skip(if:)` and `@include(if:)`. Those arguments are ordinary input values, so in real documents they come from variables the client supplied, which means the outcome is a property of the request, not of the schema or the server build. A dropped selection is not a failure — the algorithm simply does not add it — so no error is produced and there is nothing to alert on. **A type condition missed.** A fragment spread or an inline fragment carrying a type condition contributes its fields only if that condition applies to the object type actually being executed. When an interface or union field resolves to a concrete type the client did not write a branch for, every field of the non-matching fragment is absent together, while the keys selected inline at the same level are all present. That is a distinctive signature and worth learning. There is a third mechanism people wrongly suspect: collection tracks the named fragments it has already visited and skips a repeat spread of the same fragment. That guard drops no *fields*, because the first visit already collected them under the same response keys. And there is a fourth cause that is not a server-side drop at all: **the client is reading the wrong key**. If the document aliased the field, the response key is the alias, and code looking for the field name finds nothing. ## A worked incident A farm sensor dashboard renders one tile per paddock. At a peak of about 1,200 requests per minute, three of forty-three paddock tiles started rendering their moisture row blank — not zero, blank — for part of the user base, with no server errors and no `errors` entries anywhere in the captured responses. The first hypothesis was the morning's deploy: a client pinned to a removed field. That was ruled out in a minute, because that failure looks completely different. A document selecting a field the schema no longer declares fails validation and comes back as a **request error** with no `data` key at all — loud, total, and identical for every caller. Here `data` was fully populated and only one key was missing. The actual cause was in the document. The tile selects `soilMoisture @include(if: $withMoisture)`, and a feature-flag rollout was sending `$withMoisture: false` for a cohort. The field was never collected, so no resolver ran and nothing was logged. The only artefact that recorded the decision was the variables the client sent, which the service was not logging. ## The diagnosis order 1. **Absent or null?** Read the raw body, not a client-side model. This single distinction chooses your entire path. 2. **Reproduce with the caller's exact operation and variables**, not a hand-typed approximation — retyping the document is how a `@include` flag or an alias quietly disappears from the repro. 3. **Read the directives** on the missing selection and the variable values that fed them. 4. **Add `__typename`** to the enclosing selection set. If the runtime type is not the fragment's type condition, that is the answer, and it will explain a whole group of missing keys at once. 5. **Check the response keys the document actually asked for** against the keys the client reads. 6. **Only now** look at `errors` and server logs — and if you are here, the key was probably present as `null` all along. ## What to build afterwards Log the operation name and the variable *shape* per request so a conditional selection is reconstructible later. Have clients treat an absent key as a distinct case rather than coercing it to `null`. And decide deliberately how much conditional selection you want in your clients' documents, because every `@include` is a way for part of your graph to go quietly missing without anything on the server noticing.
- Why is no errors entry produced when a field is excluded by @skip?Because exclusion is not a failure. The collection step evaluates the directive's `if` argument and simply does not add the selection to the grouped field set. There is no resolver call to raise, and the spec defines no error for a selection the client asked to exclude. That silence is the diagnostic problem: the only record of the decision is the variable values the client sent.
- How do you tell a fragment type-condition miss from an excluded field, given the same symptom?Add `__typename` to the enclosing selection set and re-run with the caller's exact variables. A type-condition miss shows a concrete type that is not the fragment's condition, and every field from that fragment is absent together while keys selected inline at the same level are all present. An excluded field goes missing regardless of the runtime type, and the culprit is visible in the variables.
- Could spreading the same named fragment twice at one level be why a key is missing?No. Collection tracks the named fragments it has already visited and skips a repeat spread of the same fragment, but that fragment's fields were collected on the first visit and appear under exactly the same response keys. The guard exists to bound the walk, not to drop anything observable. Look at the directives and the type conditions instead.
saying these in an interview costs you the question
- Treats a missing key and a null value as the same thing
- Hunts for an errors entry that an excluded field never produces
- Debugs against the schema instead of the document and variables
- Assumes an excluded field still calls its resolver
- Blames a resolver returning null when no resolver ran
- Forgets the client may be reading an aliased key