DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Generated Clients and Error Responses: Typing 4xx/5xx Bodies Instead of Throwing Blind

Quick answer

A generated client that only models successful responses leaves your error handling untyped. Most generators turn 2xx schemas into clean return types but collapse every 4xx and 5xx into a generic exception, so the structured error body the server documents is available only as an untyped blob you parse by hand. That pushes real logic into runtime string matching and defeats the point of code generation. Treat error responses as part of the contract: define schemas for the failure statuses, configure the generator to emit typed error types or a discriminated result, and handle the documented failures at the call site with the same type safety you get on the success path.

What an untyped failure looks like

An endpoint documents a 422 validation error and a 404, each with a defined body, but the generated client surfaces them through one generic exception.

The OpenAPI already describes that body precisely, including the fields that distinguish a field-level validation failure from a missing resource.

If the generator ignores those schemas, the information exists in the contract but disappears in the code, and every caller re-derives it with ad hoc parsing.

Why generators under-model errors

Success types are the obvious deliverable, and error responses historically carried less structured detail. Generators optimized for the happy path and treated failures as transport exceptions. That made sense when error bodies were free-form HTML, but modern APIs return structured, schema-defined errors, often following a shared problem format. The generator's generic exception is now a lossy representation rather than a neutral one, and the loss is invisible until a caller needs to branch on a specific error.

Approach Error body at the call site Refactor safety
Generic exception, untyped body Unknown; manual parsing None; field renames break silently
Typed exception per status Strongly typed schema Compiler flags changed fields
Discriminated result union Success or error variants, no throw Exhaustive handling enforced
Shared error base with extensions Common fields plus per-status detail Good balance for many endpoints

Configuring the generator for typed errors

The exact mechanism depends on the generator, but the prerequisite is the same: give every failure response a real schema rather than an empty or free-form body. Reuse a shared error base so common fields like type, title, and requestId are defined once, and extend it with status-specific fields. Then enable the generator's typed-error or exception-generation option so those schemas become code.

With a shared base, callers can read common fields without narrowing, and inspect the typed extension only when handling a specific status. That keeps everyday error handling concise while preserving full detail where it matters.

Result unions versus exceptions

Some generators and languages prefer a result object whose variants encode success and each error, so failures are values rather than thrown exceptions. The advantage is that exhaustiveness becomes a compile-time check: adding a new documented error status forces callers to consider it. Thrown exceptions are more familiar but rely on the developer remembering to catch each typed error. Choose the model that matches your language's conventions, but in either case insist that the error body be typed from the schema rather than parsed as an opaque string.

Verifying the generated error path

Typed errors are only trustworthy if they are exercised. Add tests that trigger each documented failure status and assert the generated client produces the right typed error with populated fields, not just that something threw. A contract test against a mock returning the exact 422 schema confirms the generator's mapping and catches a regression where a generator update silently reverts to generic exceptions. Treat the error mapping as part of the generated output you validate, not an assumed feature.

FAQ

Do I need a schema for every error status?

For any status a caller might branch on, yes. Statuses that are truly unrecoverable and never inspected can share a generic error, but the moment you need to read a field or distinguish cases, define a schema so the generator can type it.

What if the server returns undocumented errors?

Keep a generic fallback exception for unexpected statuses, but do not let it replace the typed handling of documented ones. The fallback is a safety net; the contract should still be expanded when a real recurring error is discovered so it becomes typed.

Is a shared problem format worth adopting?

Yes. A consistent error shape, whether RFC 9457 or your own, lets you define common fields once and reuse them across endpoints, which makes typed error generation far cleaner and reduces per-endpoint duplication.

Will typed errors add a lot of generated code?

Reusing a shared base keeps the volume modest, and the cost is small compared with the manual parsing and fragile string matching it removes. The generated variants also centralize the mapping so individual callers stay lean.

This is part of the The Code Generation Guide — start there for the full approach to generating clients that stay trustworthy beyond the happy path.

Top comments (0)