RFC 9457 · one error, built layer by layer

Errors need a language. application/problem+json

Follow one rejected order from a bare 422 to a safe, reusable error contract. Each chapter keeps the same example and adds exactly one capability.

01

Start with the gap

A status code is useful—but broad.

A customer submits an order. The server rejects it. Watch what the client can and cannot infer from the HTTP layer alone.

Same order · 1/7 Start with HTTP: 422 only Next: make the body recognizable →
Attempt 1 · only HTTP semantics Illustrative API at api.example.test
C
Shop client Needs to help the customer correct the order.
request → JSON
POST /orders
Content-Type: application/json

{"sku":"MUG-42","quantity":0}
← response HTTP
HTTP/1.1 422 Unprocessable Content
S
Orders API Understands exactly which input rule failed.

422

The request content was understood, but could not be processed.

Which field failed? The client cannot tell.
How can it be fixed? The client cannot tell.
Can code react? Only to the broad class “422”.
Predict: should the server invent a new HTTP status?

No. Keep the correct standard status for generic HTTP software, and put domain-specific meaning in the response representation.

→

The opening: HTTP already owns transport-level meaning. The missing piece is a consistent body for richer API-level meaning.

02

Add a recognizable body

Start with the smallest useful problem.

The status stays 422. Add the Problem Details media type and human context first. With no explicit type, the problem defaults to about:blank: it means no more than the HTTP status.

Same order · 2/7 422 + recognizable problem body Next: add stable identity →
← HTTP response layer 2 · generic problem
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "title": "Unprocessable Content",
  "status": 422,
  "detail": "Quantity must be at least 1."
}

What the media type says

“This JSON object follows the Problem Details data model.” It is a representation label, not an HTTP status and not an exception class.

What is required?

RFC 9457 defines five standard members, but it does not require every document to contain all five. Include only the members that make this response useful.

Who reads what?

Proxies and generic libraries use the HTTP status. Application clients can additionally understand the problem body.

1

This body is useful to a person, but still generic to code. It explains the 422 consistently; it does not yet name a reusable application-specific problem.

03

The identity lesson

A problem type repeats. An instance does not.

Retry the same bad order. Its reusable category stays stable, while each failed request gets its own occurrence identifier.

Same order · 3/7 Problem body + type + instance Next: add machine-readable field data →
attempt A quantity: 0
{
  "type": ".../invalid-order",
  "title": "Order is invalid",
  "status": 422,
  "detail": "Quantity must be at least 1.",
  "instance": ".../problems/01K42"
}
same
type · title · status · detail
changes
instance
same type
different event
attempt B same request retried
{
  "type": ".../invalid-order",
  "title": "Order is invalid",
  "status": 422,
  "detail": "Quantity must be at least 1.",
  "instance": ".../problems/01K43"
}
same
type · title · status · detail
changes
instance
Client branches on type

Is this .../invalid-order?

client behaviour
if (problem.type === INVALID_ORDER) {
  showOrderEditor();
  showMessage(problem.detail);
}

type is a URI, not necessarily a fetch

It is the primary identifier. An HTTPS type URI should resolve to human-readable documentation, but clients should not automatically fetch it while handling every error.

instance may be opaque

It can resolve to occurrence information, or simply act as a unique server-significant identifier. Absolute URIs avoid relative-resolution surprises. Do not assume it exposes public debug data.

!

Never parse detail to drive logic. It is human text and may change or be localized. Branch on type; use documented extension members for structured values.

04

Let machines help the human

Extensions carry problem-specific data.

One invalid order can contain several validation issues of the same problem type. Add an errors member so the client can place each message beside the right input.

Same order · 4/7 Identity + documented errors extension Next: implement the contract locally →
422 problem response standard + extension
{
  "type": "https://api.example.test/problems/invalid-order",
  "title": "Order is invalid",
  "status": 422,
  "detail": "Correct the highlighted fields.",
  "instance": "https://api.example.test/problems/01K44",
  "errors": [
    {
      "pointer": "#/quantity",
      "code": "minimum",
      "detail": "Must be at least 1."
    },
    {
      "pointer": "#/delivery/postcode",
      "code": "format",
      "detail": "Use four digits."
    }
  ]
}
5 standard members errors extension

The type defines the extension

Documentation for .../invalid-order should define the errors shape, meaning, and client expectations. Unknown extension members must be ignored so the model can evolve.

Result in the client

quantity

Must be at least 1.

postcode

Use four digits.

The client reads structure; the customer reads prose.

Several related issues

An errors array is a good fit when each item belongs to the same validation problem type. RFC 9457 includes this pattern.

Several unrelated problems

Do not casually turn one HTTP failure into a grab bag of unrelated problem types. The RFC recommends representing the most relevant or urgent problem; true batch APIs need deliberately defined batch semantics.

05

Implementation scale · local

Start on one endpoint.

A local implementation proves the wire contract. This is perfectly reasonable for one route or an incremental rollout.

Same order · 5/7 Complete contract in one order route Next: separate domain meaning from shared mechanics →
C
Order client Recognizes one problem type from one route.
request → orders only
POST /orders
← response problem details
422 application/problem+json
type: .../invalid-order
S
Order handler Builds the complete problem response itself.
server · illustrative pseudocode local construction
async function createOrder(request) {
  const issues = validate(await request.json());

  if (issues.length) {
    return json({
      type: TYPES.invalidOrder,
      title: "Order is invalid",
      status: 422,
      detail: "Correct the highlighted fields.",
      instance: newProblemId(),
      errors: issues
    }, {
      status: 422,
      headers: {
        "Content-Type": "application/problem+json"
      }
    });
  }

  return createTheOrder();
}

What this teaches

The contract is already useful.

  • The HTTP status and body agree.
  • The media type makes the representation recognizable.
  • The type is stable and documented.
  • The client receives actionable validation structure.

Local construction is not wrong. Its cost appears when every route repeats policy decisions.

Predict: what drifts when 30 handlers copy this block?

Media types, status/body consistency, instance generation, safe detail text, tracing, localization, and extension conventions can all diverge. The next design centralizes those cross-cutting decisions while each endpoint still chooses its domain problem type.

06

Implementation scale · shared

Reuse the model across the server.

Switch endpoints below. Domain meaning changes, but the transport contract and the client’s first parsing step stay fixed.

Same contract · 6/7 Reuse the envelope across multiple domains Next: test the trust boundaries →
Interactive server · choose a failing request Buttons work with keyboard and pointer
orders route
shared problem factory shape · headers · instance
HTTP response consistent contract
standard members errors extension
POST /orders response 422
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.test/problems/invalid-order",
  "title": "Order is invalid",
  "status": 422,
  "detail": "Correct the highlighted fields.",
  "instance": "https://api.example.test/problems/01K44",
  "errors": [
    {"pointer":"#/quantity","code":"minimum"}
  ]
}

Responsibility map

Each route
chooses type + status + safe detail + extensions
Shared model
validates core fields · sets Content-Type · keeps status aligned · generates instance · serializes JSON
Global layer
maps unexpected exceptions to a safe generic 500 problem · records private diagnostics in server logs
server · shared factory illustrative pseudocode
function problem({
  type, title, status, detail, extensions = {}
}) {
  return json({
    ...extensions,
    type, title, status, detail,
    instance: newProblemId()
  }, {
    status,
    headers: {
      "Content-Type": "application/problem+json"
    }
  });
}

// Inside any route:
return problem({
  ...TYPES.invalidOrder,
  detail: "Correct the highlighted fields.",
  extensions: { errors: issues }
});
client · generic first step all endpoints
async function decode(response) {
  const mediaType =
    response.headers.get("Content-Type") ?? "";

  if (!response.ok &&
      mediaType.includes("application/problem+json")) {
    const problem = await response.json();

    // Stable identity chooses domain behaviour.
    const handle = handlers[problem.type];
    if (handle) handle(problem);
    else showGenericError(problem.title);
    return;
  }

  // Handle success or another representation.
}

The decoder recognizes the common representation once. Type-specific handlers still own domain behaviour.

One endpoint

Local ownership

  • Fast to introduce incrementally.
  • Simple when only one route needs rich errors.
  • Handler owns every response detail.
  • Duplication and drift rise with endpoint count.
vs

Many endpoints

Shared policy, domain-specific types

  • One generic decoder works for every client request.
  • Cross-cutting safety and observability stay consistent.
  • Routes still define meaningful domain extensions.
  • Requires governance for stable type URIs.
Predictable One media type and baseline shape.
Extensible Each problem type can add typed data.
Operable Instance IDs connect support to logs.
Decoupled Clients ignore extensions they do not know.
∴

The scalable split: centralize the mechanics of producing safe, valid Problem Details responses. Keep problem meaning—types, helpful detail, and extensions—close to the domain that owns it.

07

Failure clinic

The shape helps only if its semantics stay trustworthy.

Most mistakes come from confusing human text with machine identity, duplicating inconsistent status, or exposing implementation details.

Review · 7/7 Keep public semantics safe and dependable The complete model ✓
HTTP 400 + "status": 422 Mismatch

The server must emit the same actual status and body status. Generic HTTP software follows the status line.

if (detail.includes("quantity")) Parsing prose

Detail is for humans and can change or be localized. Use type and documented extensions for program logic.

"detail": "NullPointerException at OrderDao:71" Leaking internals

Problem Details is not a debugging transport. Keep stack traces, queries, and sensitive data in protected server diagnostics.

"type": "invalid-order-42" Unstable identity

A type is a URI reference identifying a reusable class. Do not create a fresh type for every occurrence; that is the instance’s job.

Content-Type: application/json Hiding the representation

A body may look similar, but the registered media type tells a generic client that this is a Problem Details document.

one type per wording change Too many types

Create a new type when clients need distinct semantics or handling—not merely because the sentence changed.

Important topics to learn next

A practical study map

  1. HTTP status semantics and response headers
  2. The five standard members and their defaults
  3. Problem type design and stable URI ownership
  4. Problem occurrence / instance identifiers
  5. Extension schemas and forward compatibility
  6. Validation and batch-error modelling
  7. Content and language negotiation
  8. Security, privacy, logging, and trace correlation
  9. Shared server factories and generic client decoders

Reconstruct the model

status

What broad HTTP outcome occurred?

type

What reusable kind of problem is this?

instance

Which concrete occurrence are we discussing?

detail

What helpful human explanation applies now?

extensions

What structured data does this problem type promise?

✓

In one sentence: application/problem+json is the JSON representation of the Problem Details model—a common envelope that preserves HTTP semantics, identifies reusable problem types and individual occurrences, and allows documented domain extensions.