Skip to content

Core concepts

Artifact: problem-details-core. No dependencies, no framework — plain Java 17 and up. Everything else in this library builds on what's here.

The five fields

Errors have the same five fields everywhere, with the RFC's names:

Field Example Notes
type https://example.com/probs/out-of-credit What kind of problem. A URI you control. Defaults to about:blank.
title You do not have enough credit. Short summary. Same for every occurrence (translations aside).
status 403 The HTTP status. Handy to have inside the body, but the response line stays authoritative.
detail Your current balance is 30, but that costs 50. What happened this time. Written for humans, never parsed by machines.
instance /account/12345/msgs/abc Which occurrence this is.

Think of it as two halves: type + title describe the kind of problem (stable, reusable), while status + detail + instance describe this time it happened.

type and instance accept plain strings — relative references like /types/out-of-credit work fine.

Your own fields

Real errors carry their own data. Instead of a loose map, each field gets a key that pairs its name with its Java type, so reads are checked by the compiler:

ProblemDetailKey<Integer> BALANCE = ProblemDetailKey.of("balance", Integer.class);

ProblemDetail problem = ProblemDetail.builder()
    .extension(BALANCE, 30)
    .build();

int balance = problem.get(BALANCE).orElseThrow(); // no casts, ever

Missing fields come back empty (Optional), has(key) tests presence, and putting two values under one name keeps the last — the name is what identifies the field wherever it travels.

Built problems never change

Once built, a problem is frozen: no setters, and everything you read out is unmodifiable. Builders are single-use in spirit — build on one thread, share the result on all of them.

Throwing problems

ProblemDetailException is an unchecked exception that carries the whole structured error:

throw new ProblemDetailException(problem);

Handlers read getProblemDetail() — the exception message is only there so stack traces stay readable. The Spring and JAX-RS integrations turn these exceptions into HTTP responses for you.