Every API invents its own error shape: { "error": "..." } here, { "message": "...", "code": 17 } there, sometimes a bare string. Clients end up with special-case parsing for each service they call. RFC 9457, Problem Details for HTTP APIs, defines one machine-readable error format. In this lesson you will learn its members, how to extend it for validation errors, and how to produce it from a single Express error middleware.
An HTTP status code says what kind of failure occurred, not why. 403 could mean an expired subscription or a missing scope; 422 could mean any of twenty validation rules. Problem Details gives every error a stable identity (type) plus a human-readable explanation, so clients can branch on type and show detail to users. The standardized media type application/problem+json lets SDK generators and logging tools recognize it too.
| Member | Type | Meaning |
|---|---|---|
| type | URI | Error category; defaults to about:blank when the status code says it all |
| title | string | Short summary, identical for every occurrence of this type |
| status | integer | The HTTP status code, repeated for convenience |
| detail | string | Human-readable explanation of this occurrence |
| instance | URI | Identifies this occurrence, often the request path or a trace id |
The type URI need not resolve, but serving documentation there is good practice. Any other member is an extension; clients must ignore members they do not understand.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/subscription-expired",
"title": "Subscription expired",
"status": 403,
"detail": "Your Team plan expired on 2026-09-01. Renew to keep creating projects.",
"instance": "/projects",
"renewUrl": "/billing"
}Validation failures usually involve several fields at once. Add an errors array as an extension member, one entry per field, using a JSON Pointer so the client can highlight the right input:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Request body is invalid",
"status": 422,
"errors": [
{ "pointer": "/email", "message": "must be a valid email address" },
{ "pointer": "/age", "message": "must be at least 13" }
]
}This shape is what most validation libraries produce with a small mapping function, and it keeps the top-level structure identical to every other error.
Define one error class and let a single error-handling middleware format every failure:
class Problem extends Error {
constructor(status, title, detail, extra = {}) {
super(detail);
Object.assign(this, { status, title, detail, ...extra });
}
}
app.get("/books/:id", (req, res, next) => {
const book = db.get(req.params.id);
if (!book) return next(new Problem(404, "Book not found", `No book with id ${req.params.id}`));
res.json(book);
});
app.use((err, req, res, next) => {
const status = err.status ?? 500;
const { title = "Internal Server Error", detail, ...extra } = status === 500 ? {} : err;
res.status(status).type("application/problem+json").json({
type: err.type ?? "about:blank",
title,
status,
detail: status === 500 ? undefined : detail,
instance: req.originalUrl,
...extra,
});
});Unexpected exceptions become a generic 500 with no detail, so stack traces and database messages never leak. Log the original error with a request id and put that id in instance so support can correlate reports with logs.
Clients should check Content-Type before parsing: application/problem+json means the body follows this contract, even for a status they did not anticipate. Branch on type for decisions and display detail to the user.
title constant per type; put the changing information in detail and extensions.422 for semantic validation failures and 400 for unparsable requests.Which `Content-Type` marks a response body as a Problem Details object?
type, title, status, detail and instance.Content-Type: application/problem+json so clients can detect it reliably.errors array carry field-level validation information.500.type and show detail; they never depend on the wording of title.Next lesson: HTTP Caching: ETag, Cache-Control and Conditional Requests — make repeated reads cheap and protect writes from lost updates.