Skip to content

PATTERN Cited by 1 source

Problem type for customer-actionable errors

Pattern

When the only party that can recover from a failure is the end user (the Customer), model that failure as a schema-level Problem type — a GraphQL object type that appears as an arm of a Result union — rather than as an entry in response.errors.

Shape:

type RegisterProblem {
  "translated message encompassing all invalid inputs."
  title: String!
  invalidInputs: [RegisterInvalidInput]
}

type RegisterInvalidInput {
  field: RegisterInvalidInputField!
  "translated message."
  message: String!
}

enum RegisterInvalidInputField {
  EMAIL
  PASSWORD
}

The name Problem (not Error) is borrowed from RFC 7807. See concepts/problem-vs-error-distinction.

What the schema buys you

Because the Problem is a first-class schema type:

  • Codegen produces typed client classes for RegisterProblem, RegisterInvalidInput, and the enum — parseable by construction, no string matching.
  • Schema review applies: adding a new RegisterInvalidInputField goes through the same CR process as adding a new data field.
  • Introspection documentation surfaces the error vocabulary.
  • i18n payload (the title and per-input message fields) gets a typed home; the client UI renders the strings directly next to the form fields.
  • Deprecation tracking applies to error vocabulary the way it applies to data fields.

The tight scope

Zalando is explicit that this pattern should be used in one canonical situation: mutation input validation.

"Mutation Inputs is the only case where it is crucial to construct Problem types. It contains inputs directly from the Customer, and only the Customer can take action for this. So, we model these errors as Problems and not Errors."

(Source: sources/2021-04-12-zalando-modeling-errors-in-graphql)

Overusing Problem types (e.g. wrapping every query in a ...Success | ...Error union) forces the client into ... on fragment spreads at every level of the tree, destroying GraphQL's query-shape ergonomics. See the worked counter-example in concepts/graphql-error-propagation.

Decision rule

The pattern fires when all of the following hold:

  1. The Customer is the action-taker (not the Developer, not an operator) — per concepts/error-action-taker-classification.
  2. The error is not a bug — a runtime exception or infrastructure failure stays an Error.
  3. The error must not propagate. Returning it as a Problem means the caller sees both arms of the union; GraphQL's null-propagation does not fire.
  4. The failure payload has structure — per-field codes, translated messages, nested shape — that justifies the schema weight.

Cost

  • Every caller of the mutation must spread both union arms.
  • Every new Problem type needs a schema CR.
  • The i18n catalogue must be wired into the resolver layer (the title and message fields are resolver-rendered, locale-aware strings).

Pairs with

Seen in

Last updated · 476 distilled / 1,218 read