In a server-side web framework, what does a central exception-to-status handler registry do, and why prefer it to per-route catches?
answer
- failure type in, status out
- one table at the request boundary
- framework catches around your handler
- nearest registered ancestor wins
- handlers stop knowing about transport
basics
~20 sA central registry maps failure types to HTTP statuses in one place: the framework catches whatever a handler throws, looks up the closest registered type, and runs that mapper to build the response. Route code stays free of transport concerns.
solid answer
~50 sA central exception handler registry is a lookup from failure type to a small function that turns that failure into a response. The framework invokes your route handler inside its own catch at the request boundary; when anything throws, it resolves the thrown object's type against the registry — an exact registration first, otherwise the nearest registered ancestor — and runs that mapper, which decides the status. Centralising it means one kind of failure maps to one status everywhere, deep domain code never has to know it is inside a web request, and failures the framework itself raises before your code runs (no route matched, a body that will not parse) can be mapped through the same table. A `try`/`catch` in each route only covers what that route calls, and duplicates the decision in every file.
go deeper
Recall the shape: your code throws a typed failure, the framework catches it at the boundary and looks the type up in one table to pick a status. Do not build error responses by hand inside route handlers.
Explain the lookup itself — exact type first, then nearest registered ancestor — and why the registry also sees failures raised by routing and body binding that a handler-local catch never reaches.
Show the operational side: one log per failure at the boundary with a correlation identifier, severity split between caller mistakes and dependency failures, and a deliberately narrow set of mapped types so real defects stay visible.
Frame it as contract ownership. The registry is where an organisation makes 'this kind of failure means this status' a single enforceable decision instead of a per-team habit, and where drift becomes reviewable.
## Why a translation layer exists at all A request handler fails for reasons that have nothing to do with HTTP: a record is missing, an invariant is violated, a permission check says no, a downstream call times out. The code that detects the failure usually sits several frames below the code that writes the response, and it should not have to know it is running inside a web request. So it signals the failure by throwing a **typed failure object**, and something closer to the transport boundary translates that object into a status code and a body. A **central exception-to-status registry** is that translation layer: a lookup keyed by failure type whose values are small mapping functions, consulted by the framework at the very point where it invoked your handler. ## How the lookup runs 1. The router matches a request and the framework invokes the matched handler **inside its own catch**. 2. The handler — or anything it calls, however deep — throws. 3. The framework catches at that boundary and takes the thrown object's runtime type. 4. It resolves that type against the registry: an **exact** registration wins; otherwise the **nearest registered ancestor** of that type wins. 5. The selected mapper runs, usually receiving the failure plus the request context, and yields the status the response will carry. 6. If nothing matches, the failure falls through to whatever the framework does by default. Registration style differs: some frameworks let you declare a mapper against a type, others have you append entries to a list at startup, others accept a predicate plus a mapper. The *mechanism* is the same in all of them — one table, consulted once, at the boundary. ## What it buys over a catch in every handler - **One decision, one place.** "Record not found" becomes the same status in every endpoint, instead of 404 in one file and 400 in the next because two people wrote two catches. - **Handlers stay about the happy path.** The success path reads as a straight line; the failure path is declared elsewhere, once. - **It covers failures your catch never sees.** Routing, body binding and serialization failures are raised by the framework *around* your handler body, so a `try`/`catch` inside that body is never entered for them — the registry is. - **Cross-cutting concerns land in one spot.** Logging a failure at the right level, attaching a correlation identifier, and incrementing an error metric happen once at the boundary rather than in dozens of catch blocks. - **New failure types are cheap.** Adding a domain failure means adding one registration, not editing every endpoint that can raise it. | Axis | A catch in every route | Central registry | |---|---|---| | Where mapping lives | Scattered across handlers | One table | | Consistency across endpoints | Drifts as the code grows | Uniform by construction | | Framework-raised failures | Not covered | Covered | | Route-specific special cases | Natural | Needs a scoped handler or a distinct type | | Handler readability | Mixed success and failure code | Success path only | ## What belongs inside a mapper A mapper should be close to a pure translation: take the failure, decide the status, hand back a response. Things to keep out of it: - **Business decisions.** If the mapper is choosing between outcomes, that logic belongs in the domain, expressed as two different failure types. - **Work that can itself fail.** A mapper that queries a database or calls a remote service can throw while handling a throw, which is the worst place to be. - **Per-call logging noise.** Log once, at the boundary, at a level that matches the class of failure — caller mistakes are not the same severity as a broken dependency. - **Internal detail leaking outward.** The mapper decides the *status*; the exact shape and content of the failure body is a separate contract, and stack traces are not part of it. ## What the registry does not decide Two things are often confused with it. First, **which status a given situation deserves** is an API-design question answered by the HTTP protocol and your API standard — the registry only guarantees that the answer is applied consistently once you have made it. Second, the registry is not a safety net for bugs: mapping every failure to a tidy response can hide a defect that should have been loud. A good setup maps the failures you have *modelled* and lets the rest go to a single, deliberately unglamorous server-error path that is monitored. The practical payoff is the one the leaf is named for: because the lookup picks the nearest registered ancestor, the registry is also the place where a careless broad registration can capture failures that had a perfectly good status of their own — which is why teams learn to register narrow types first and to treat the broadest registration as a last resort rather than the default.
- Why can a central mapper handle a failure that a catch inside a route handler cannot?Because routing, body binding and serialization failures are raised by the framework *around* the handler, before or after its body runs. A catch written inside the body is never entered for them, while the registry sits outside that body and sees everything the boundary catches.
- Should a mapper ever do I/O, such as looking up a friendlier message?Avoid it. A mapper runs while a failure is already in flight, so anything that can throw or block there risks turning a clean 4xx into a hung request or a second failure with no handler left to catch it. Precompute what you need, or carry it on the failure object.
- How do you keep the registry from silently swallowing real bugs?Only map failure types you have deliberately modelled. Leave everything else to the single broad server-error path, log it at error level with a correlation identifier, and alert on its rate — a registry that maps everything to something tidy makes a defect look like normal traffic.
It is the mail room rather than each desk: staff drop an addressed envelope in an outbox, and one room decides what postage it needs, so identical letters never leave the building with different stamps.
saying these in an interview costs you the question
- Says each route handler should catch and build its own error response
- Claims the registry decides which status a situation deserves, so no API policy is needed
- Thinks one registration on the broadest failure type is all the mapping a service needs
- Puts business branching or extra data fetching inside the exception mapper
- Assumes a catch inside the handler body also covers routing and body-binding failures