When is translating every library exception into a domain hierarchy the wrong call for a 4-person team?
answer
- Count the callers on the other side
- Cost is a mapping that must be maintained
- Ask whether the dependency will really change
- Beware the wrapper that leaks vendor detail
- Decide per boundary, not per module
basics
~20 sWhen the mapping costs more than the coupling it removes: one application, one team, one library that will not be replaced. Translation earns its keep at boundaries someone else calls or where the dependency is genuinely swappable - not at every internal module edge.
solid answer
~50 sTranslation buys independence from a dependency and costs a mapping that must be maintained as both sides change. For a 4-person team on a single deployable application with one database driver it is nowhere near free: every new failure mode is a design decision, every wrapper is a class to name and document, and over-wrapping produces a hierarchy so generic that callers fall back to `except Exception` anyway. I would apply it at the boundaries that actually have a second side - a published package, an adapter behind an interface with more than one implementation, a service consumed by another team - and elsewhere let well-known types such as `OSError` and `TimeoutError` travel, with a single package-level base for the errors we invent. The failure mode to watch is the wrapper that leaks: a domain exception carrying a vendor error code protects nothing, because callers now depend on the library through your class.
go deeper
You are not expected to set this policy, but know that wrapping is a tradeoff rather than a rule, and that inside one small application letting a familiar type like OSError travel is often the right call.
Be able to argue both sides for a specific module: who calls it, whether the dependency could realistically be replaced, and what a wrapper would add beyond a renamed error.
Show the failure modes you have seen - hierarchies nobody catches, wrappers leaking vendor detail, a mapping that stopped covering new library errors - and how you would keep the boundary honest with a check rather than a convention.
Own the policy across the codebase: which boundaries are real, what the mapping costs a small team over years, how you enforce it mechanically, and when you would deliberately decide not to have an exception hierarchy at all.
## Translation is a purchase, not a virtue Wrapping a library's exceptions in your own types buys one thing: callers stop naming a dependency you might replace. It costs a mapping - a piece of code that must know every failure mode the library can produce and what each one means to you - and the mapping ages as both the library and your domain change. Whether the purchase is worth it depends entirely on how many callers exist, how likely the dependency is to change, and how many people are available to maintain the layer. For a 4-person team shipping one deployable application against one database driver, the honest answer is often "at two boundaries, not at twelve". ## Where translation clearly pays * **A published package.** Consumers you cannot edit must not have to import your dependencies to catch your errors, and you cannot change your mind cheaply later. * **A genuinely swappable adapter.** If two implementations exist - or one exists and a second is on the roadmap - the shared vocabulary must be yours or the two implementations are not interchangeable. * **A cross-team or cross-process edge.** Errors that leave the process cannot carry a library's classes reliably anyway, so a domain vocabulary is forced. * **A dependency in a message, not just a type.** Where the library's error text carries paths, hosts or credentials, a boundary that restates the failure in your own words is a containment measure as much as a design one. ## Where it usually does not * **Module edges inside one application.** Both sides are edited by the same four people in the same commit. The wrapper adds a class, a mapping and a `__cause__` link, and removes coupling nobody was going to trip over. * **Stdlib vocabulary the caller already knows.** `OSError`, `TimeoutError`, `ValueError`, `KeyError` are shared language. Hiding them behind `AppIOError` forces every reader to look up what it actually means. * **A dependency that will not be replaced.** Nobody rewrites the database layer of a small internal tool. The abstraction is being paid for against a future that will not arrive. * **Errors that represent bugs.** Wrapping `AttributeError` or `TypeError` from your own code turns a stack trace into a mystery. ## The three ways this goes wrong at scale **Over-wrapping into uselessness.** A hierarchy where every failure ends up as one generic application error leaves callers with `except AppError` - functionally identical to `except Exception`, but with a maintenance burden attached. If nobody branches on the types, the hierarchy is decoration. **The leaky wrapper.** A domain exception with a vendor error number, a driver cursor or a library object among its attributes couples callers to the dependency through the class you invented to hide it. When you replace the library, the wrapper's attributes change and every caller breaks - so you have paid for the abstraction and kept the coupling. Keep the library's detail on `__cause__`, where it is available for logging and inspection but not part of the type's contract. **Mapping drift.** The library adds a failure mode; your `except` clause names classes that do not cover it; the new error escapes raw through a boundary that promised it would not. Catching the library's base error class rather than a list of leaf types is the cheap defence, and it should be the reviewed default wherever a boundary exists. ## How I would decide it as a policy Make it a per-boundary decision with a written rule, not a per-module habit: 1. **Enumerate the real boundaries.** Usually far fewer than the number of packages. Anything with exactly one caller inside the same repository is probably not one. 2. **At each, ask what the caller does differently** for each failure. That number is the number of domain types. Zero different reactions means no hierarchy is needed at all. 3. **Fix the leak rule:** no library object, error code or module import may appear in the wrapper's attributes or in its module's public names. 4. **Make it enforceable rather than cultural.** An import rule that fails the build when a module outside the adapter package imports the driver is worth more than a paragraph in a wiki, particularly on a small team where the design conversation happened once and the joiner two years later never heard it. 5. **Budget the maintenance.** If nobody will update the mapping when the library changes, the layer is a liability that hides new failures rather than an abstraction. ## The tradeoff stated plainly A small team's scarcest resource is attention, and every abstraction consumes some forever. Translating exceptions is one of the cheapest abstractions available and still not free. The defensible position in an interview is not "always wrap" or "never wrap": it is being able to point at a specific boundary, say who is on the other side of it, and explain what would break if the dependency changed tomorrow. Where that answer is "nothing, we would edit both sides in one commit", the wrapper is ceremony.
- What is the minimum error design you would still insist on for an internal application with no wrapping?One package-level base exception for the errors the application itself raises, so a caller can distinguish "our code decided this" from "something underneath blew up", plus a rule that library exceptions never escape a module the rest of the codebase treats as a boundary. Beyond that, let stdlib types travel. It costs one class and gives the single distinction that actually gets used.
- How would you recognise that a team's exception hierarchy has become decoration?Look at the handlers, not the classes. If almost every `except` in the codebase names the base type or `Exception`, nobody is branching on the hierarchy and it is providing no value for its maintenance cost. A second symptom is wrappers whose messages restate the cause verbatim - a rename with a `__cause__` attached, not a translation.
- A wrapper exception carries the driver's numeric error code as an attribute. Why does that undermine the whole exercise?Because callers will branch on it, and that code is the library's vocabulary. You now have a domain type whose contract includes a dependency's detail, so replacing the library breaks every caller that reads the attribute - exactly the outcome the wrapper existed to prevent. Keep such detail on `__cause__`, where logs and diagnostics can reach it without it becoming part of the type's promise.
saying these in an interview costs you the question
- Treats wrapping every dependency error as unconditionally correct
- Cannot name a cost of the translation layer
- Designs a deep hierarchy nobody branches on
- Puts vendor error codes on the domain exception
- Wraps stdlib types like OSError with no boundary to justify it
- Relies on a wiki convention instead of an enforceable check