One root exception lets you attach metadata once, render one JSON error response shape, and decide which exceptions are catchable by middleware vs which surface 500s. Per-domain subclasses (RepositoryError, ServiceError, RouterError) tag where it came from without try/except ladders.
class AppError(Exception): pass; class RepositoryError(AppError): pass.
Middleware catches AppError -> JSON {error_code, message, request_id}; other exceptions -> 500.
Adding fields (retry_after, severity) is centralized in the renderer.
raise RepositoryError("not found")
middleware catches AppError -> JSON {error_code, ...}
Other exceptions not caught -> 500 with trace_id
One exception tree + one renderer beats N try/excepts scattered across routes.