In a framework offering both route-scoped and global exception handlers, how do the two interact, and what does rethrowing from a handler do?
answer
- registries nest like rings
- innermost match wins first
- no match means fall through outward
- rethrow resumes the outward walk
- scoped catch-all steals global mappings
basics
~20 sScopes nest: the innermost registry that covers the failing route is consulted first, and a failure with no match there falls through outward to the global one. Rethrowing from a mapper typically resumes that outward fall-through.
solid answer
~50 sHandler registries usually nest with the routing structure: a registry attached to one route or route group is consulted first, and the global registry acts as the outer ring. Resolution is therefore two-dimensional — **innermost scope that has a match wins**, and within a scope the usual type-specificity or ordering rule applies. A scoped handler that finds no matching registration does not end the story; the failure **falls through** to the next scope outward, and finally to the framework's default if nothing claims it. Rethrowing from inside a mapper (or signalling "not handled", where the framework offers that) normally resumes the same outward walk, which is how you write a scoped handler that special-cases one failure and lets everything else keep the shared mapping. Throwing a *different* failure from a mapper is riskier: frameworks commonly refuse to re-enter the registry to avoid a loop.
go deeper
Know that handlers can be attached to one route or to the whole application, and that the more local one is tried first. Prefer the global one unless a route genuinely differs.
Explain the two stacked rules — scope first, then specificity inside the scope — and describe fall-through and rethrow as the mechanism that lets a scoped handler special-case one type and inherit the rest.
Demonstrate the failure mode you have seen: a scoped catch-all silently overriding the service-wide contract, and mappers that themselves throw and so bypass the registry entirely. Say how you would detect both.
Decide the policy: which scopes are allowed to register at all, that catch-alls exist only at the outermost ring, and that boundary concerns like logging and correlation are owned globally so they cannot be double-counted.
## Scopes form a chain, not a flat table A framework that supports scoped handlers effectively gives you several registries arranged as rings around the handler: the route, perhaps a route group or module, and the application-wide registry outermost. When a failure escapes the route handler, resolution proceeds: 1. Consult the **innermost** registry that covers the failing route. 2. Inside that registry, apply the normal rule — nearest registered ancestor, or first matching predicate. 3. If nothing there matches, move one ring outward and repeat. 4. If the outermost registry has nothing either, the failure reaches the framework's default path. So there are two rules stacked: **scope first, specificity second**. This matters because it means a *broad* registration in an inner scope beats a *narrow* one in an outer scope. A route-scoped catch-all is therefore far more dangerous than it looks: it wins over every carefully targeted global registration for that route. | | Scoped handler | Global handler | |---|---|---| | Consulted | First, innermost outward | Last, before the default | | Good for | One route's genuine special case | The service-wide contract | | Risk | A broad entry shadows the global table | Too coarse to express a local exception | | Reviewability | Easy to miss; lives next to one route | One file everyone reads | ## Fall-through and rethrow **Fall-through** is what happens when a scope has no match: the failure keeps travelling outward, unchanged. Nothing is logged as "unhandled" yet, and no status has been chosen. **Rethrow** is the explicit version, performed by a mapper that decided this failure is not its business after all. It typically resumes the outward walk from the scope that rethrew — the same object continues to the next ring. Frameworks that model handling as a boolean sometimes offer an explicit "not handled" return instead of a rethrow; the effect is the same and the return form is usually clearer. The useful pattern that falls out of this: - Register a scoped handler for the **one** failure type this route treats differently. - Let everything else fall through untouched, so the route inherits the service-wide contract for free. - Never register a scoped catch-all "just in case" — that is precisely the registration that steals the other mappings. ## Throwing something new from a mapper A mapper that throws a *different* failure is a different case from a rethrow, and it is where behaviour diverges most. Frameworks generally do **not** feed a mapper's own failure back into the registry, because a mapper that always throws would loop forever; instead the new failure usually goes to the default path, losing whatever status the original would have produced. Treat a mapper as code that must not fail: no I/O, no parsing, no lookups that can be absent. If it genuinely needs something that might not be there, compute it before the failure path or attach it to the failure object. A related trap: the ordering of hooks around the handler is not the same thing as the scope chain. Which hooks still get to see a failure once a mapper has produced a response is its own subject, and it is easy to assume a scoped mapper runs "inside" every hook when in fact the two structures are configured separately. ## When a scoped handler is actually the right tool - A route that must express a genuinely local rule — one endpoint where a particular failure means something different from everywhere else. - A route group with its own response contract, where the whole group shares a small registry. - A migration seam: temporarily overriding the shared mapping for one area while the domain types are being reshaped, with a note saying when it goes away. What is *not* a good reason: adding logging, adding a correlation identifier, or adding a metric. Those are boundary concerns and belong once, globally; duplicating them per scope produces double-counted metrics and doubled log lines, and it is the most common way a scoped handler earns its keep for a week and confuses people for a year. ## What an interviewer is listening for That you know resolution is scope-first, that fall-through is the default rather than an error, that a rethrow resumes it, and that a scoped catch-all is the registration most likely to quietly override a service-wide contract. Saying "the global one always wins because it is registered at startup" is the answer that shows the chain was never observed in practice.
- Why is a route-scoped catch-all more dangerous than a global one?Because scope is checked before specificity. A broad registration in the inner ring matches first, so it beats every narrow, deliberate registration in the global table for that route — and it sits next to one route where reviewers of the shared error contract will never see it.
- What is the difference between a mapper rethrowing and a mapper throwing a new failure?A rethrow resumes fall-through: the same object continues outward to the next scope. A brand-new failure usually is not fed back into the registry at all, because that would risk an infinite loop, so it lands on the default path and the original mapping is lost.
- Where should logging and correlation identifiers be attached when scopes are nested?Once, at the outermost boundary. Doing it inside each scoped mapper double-counts metrics and duplicates log lines for any failure that falls through, and it makes the per-failure record depend on which ring happened to claim it.
saying these in an interview costs you the question
- Says the global registry always wins because it is registered at application startup
- Adds a scoped catch-all to every route group as a safety net
- Thinks a scope with no matching registration ends the request there
- Assumes a mapper that throws a new failure is re-resolved through the registry
- Duplicates logging and metrics inside every scoped mapper