1 min read

Errors are part of the product

A failure the user can act on is a feature. How this boilerplate names domain errors once and maps them to every door — tRPC, Server Actions, REST, and the copy deck.

engineeringarchitecture

Most codebases treat errors as exhaust: something went wrong, throw a string, hope a boundary catches it. This boilerplate treats a failure the user can act on as part of the product surface, with the same rigor as a happy path.

Name the failure once

Every business failure gets a stable code — credits/insufficient, note/not-found — minted by its feature's error factory. The class is private; the code is the contract.

throw creditsErrors.insufficient({ required: 3, available: 1 })

Consumers branch on the code, never on instanceof. Codes survive serialization across any transport; class identity does not.

The registry forces completeness

A new code doesn't compile until it's mapped everywhere a user could meet it: an HTTP status, a tRPC error, and dictionary copy in every locale. Forgetting the Spanish translation is a build error, not a support ticket.

Doors translate, they don't decide

A Server Action, a tRPC procedure, and a REST handler all surface the same credits/insufficient — each in its transport's native dialect. The decision that credits were insufficient was made once, in the model.

Unexpected errors stay generic

Anything without a registered code is sanitized to a generic message. Details go to logs and error tracking; stack traces never go to the wire.

The payoff

When product asks "what can actually go wrong on checkout?", the error registry answers by reading it. That list is the honest specification of the feature — and it was free.