skip to content

In GraphQL, what does it mean to return an expected failure as data rather than as a top-level error?

level: juniorimportance: must knowfreq 54%

answer

  1. A failure can live in two places
  2. One of them is declared in the schema
  3. A union member or a payload failure field
  4. __typename tells the branches apart
  5. Convention, not a specification rule

basics

~20 s

It means the schema declares the failure as its own type — a union member or an error field inside a payload — so the failure arrives under data as an ordinary selectable value instead of in the response's top-level errors list.

solid answer

~50 s

A failure can surface in two places. A resolver can raise a **field error**, which nulls the field and adds an entry to the response's top-level `errors` list. Or the schema can declare the failure as a type, so the resolver returns it as a normal value under `data`. The second is *errors as data*. The usual shapes are a **result union** — `union ListingResult = Listing | ListingWithdrawn | ListingNotFound` — discriminated by `__typename`, or a payload object holding a nullable result beside a list of failure objects. The payoff is that the failure is documented in the schema, typed with real fields such as `withdrawnAt` instead of a prose sentence, and visible to every client and code generator. None of this is in the GraphQL specification: it is a widespread convention, and nothing validates that you followed it.

code

graphql · 20 lines
graphql
type Query {
  listing(id: ID!): ListingResult!
}

union ListingResult = Listing | ListingWithdrawn | ListingNotFound

type Listing {
  id: ID!
  address: String!
  askingPrice: Int!
}

type ListingWithdrawn {
  withdrawnAt: String!
  reason: String!
}

type ListingNotFound {
  requestedId: ID!
}

go deeper

for a junior

Be ready to say where the failure ends up in the response body and how the client gets at it. Knowing that a schema can declare a failure as a type, and that the client must select it, is the whole ask at this level.

for a middle

Explain both carriers — a result union and a payload with a failure list — and what each costs the client's selection set. Say clearly that this is a convention, not something the specification or validation enforces.

for a senior

An interviewer expects you to draw the line: which failures earn a schema type and which stay as field errors, and what swallowing a fault into data does to your error-rate metrics and alerting.

for a principal

Own the consistency problem. One graph with three failure vocabularies is worse than either style applied uniformly, so be ready to describe the house taxonomy, the shared abstract type and the review or lint gate that keeps teams on it.

## Two places a failure can live Every GraphQL response has room for a failure in two very different places, and the whole subject is about choosing between them deliberately. The server can raise a **field error** while executing a resolver. The specification's execution rules then set that field's value to `null` (or propagate further, if the field was declared Non-Null) and add an entry describing the failure to the response's top-level `errors` list. Nothing about that failure is declared anywhere in the schema; it is a runtime event, and what the client learns about it is whatever the server chose to put in that entry. Or the schema can **declare the failure as a type of its own**. The resolver then returns it as an ordinary successful value, and it arrives under `data` exactly like a `Listing` or an `Agent` would. That is what people mean by *errors as data*. Say this plainly in an interview: **the GraphQL specification does not ask you to do this.** The specification defines the response shape and the rules for raising field errors. It has no opinion whatsoever about whether "this listing was withdrawn" is a domain outcome or a fault. Errors as data is a **convention** — widespread and well argued, but invisible to validation and enforced only by the people reviewing your schema. ## The shapes it takes **A result union.** The field's type becomes a union whose members are the success type plus one type per expected failure. Members share no fields, so each failure carries exactly the data it needs, and the client discriminates on the `__typename` meta-field. ```graphql type Query { listing(id: ID!): ListingResult! } union ListingResult = Listing | ListingWithdrawn | ListingNotFound ``` **An interface instead of a union.** Same idea, but every failure implements an abstract type that declares a shared field — typically a human-readable message. The client can select that field directly and render *any* failure, including members added after its document was written, then use inline fragments only for the branches it treats specially. **A payload with a failure field.** The field returns one object type holding a nullable result beside a list of failure objects. This shape is common on mutations, uses no abstract types at all, and evolves gently — a new failure kind is a new value in an existing list rather than a new union member. It is weaker at forcing anyone's hand: the success field is nullable, and nothing stops a client from reading it and ignoring the list. ## Why teams do it **The schema documents the failure.** A `ListingWithdrawn` type appears in introspection, in generated client types and in the schema's documentation. A sentence in an errors-list entry documents nothing and is discoverable only by making the failure happen. **It is typed.** `ListingWithdrawn { withdrawnAt: String! reason: WithdrawalReason! }` gives a client real fields to render and branch on. The alternative is parsing a message string, which is the failure mode this convention exists to kill. **The client is confronted with it.** Selecting from a union means writing fragments, and a code generator turns the union into a closed set the client's own compiler can check. The failure stops being a thing you remember to handle and becomes a branch you must write. ## What it costs Every member is a permanent part of the contract: adding one is easy, removing one breaks clients. Every client pays in selection-set size — an inline fragment per branch, in every document that touches the field. And applied to the wrong failures it does real damage: if a resolver catches a dependency timeout and returns it as a data-carried failure, the response has no `errors` entry at all, so anything that counts errors sees a perfectly healthy request. The line most teams settle on: **model a failure as data when it is an expected outcome of a valid request that a client renders differently; leave everything else to be a field error.** An offer below the reserve price is a domain outcome. A geocoding call that timed out is a fault. ## What it does not change It does not change what the envelope means: `data` and `errors` keep their specified meaning, and a data-carried failure and an errors entry can appear in the same response from different fields. It does not mean the operation "failed" — the request succeeded and the value it produced happens to describe a refusal. And it does not appear by magic: a client that never selects the failure branch simply does not see it.

  • Does the GraphQL specification say anything about modelling failures as data?
    No. The specification defines `data`, `errors` and `extensions`, and the rules for raising request and field errors. Declaring a failure as a union member or a payload field is a community convention layered on top of an ordinary schema — nothing in validation or execution knows you are doing it. That is why consistency has to come from schema review and linting rather than from the language.
  • What does a client that never selects the failure branch actually receive?
    A perfectly successful response. Its document selected fragments only for the branches it knows, so the returned object contains just the meta-fields it asked for — often only `__typename`, or an empty object if it selected nothing that matched. There is no errors entry to fall back on, so the UI silently renders nothing. This is why a default branch is not optional.
  • Besides a union, what other shape can carry a failure in data?
    Two. An interface, where every failure implements an abstract type declaring a shared field such as a message, so one client branch can render any failure including ones added later. Or a payload object holding a nullable result beside a list of failure objects — no abstract types, easy to extend, but nothing forces the client to look at the list.

A field error is the shipping company phoning to say something went wrong in transit. Errors as data is the parcel arriving on time, containing a printed slip that says the item was discontinued.

saying these in an interview costs you the question

  • Claims the GraphQL specification requires result union types
  • Puts a dependency timeout in data as a domain failure
  • Thinks a failure in data means the request failed
  • Expects the failure branch without selecting it
  • Returns a bare nullable field and calls it error handling
  • Says a data-carried failure also appears in the errors list

context