Catch at the layer that knows the user-facing response. Let it bubble everywhere else. A repository should not swallow a SQLAlchemy error; the route layer translates it to a 404 or 500. This keeps the data layer pure and the response layer cohesive.
Repository raises RepositoryError(“not found”); router catches -> 404 JSON.
Service layer: try/except around a third-party library; convert to ServiceError.
Top-level middleware catches AppError; let other exceptions 500.
data layer: raise AppError
service layer: translate
router: HTTP code
top: render JSON
Catch at the boundary that turns the error into a response. Each layer translates, every other layer bubbles.