I believe this is by far the most important principle in software development. This principle states that any given object should operate at a single level of abstraction at any given time.
This concept is easier to explain with an example. Consider a banking application composed of many objects working together. Ideally, an object at the business level of abstraction should be understandable to a domain expert in banking who has no coding knowledge. Such an object can delegate work to lower-level objects through dependency inversion. It’s crucial that no details from these lower-level objects leak into the abstraction level where the high-level object operates.
Errors are also an integral part of an abstraction level, so they too must not leak between levels. For instance, when performing a money transfer, network errors should be handled at a deeper level. If we cannot handle them, the best solution is for the application to log appropriately and fail gracefully.
You can interpret this principle as follows: when a domain expert examines the code, they shouldn’t encounter code blocks dealing with network errors, because they wouldn’t understand what these are—and they shouldn’t need to, since these concerns don’t belong at that abstraction level.
Some concerns, like logging, are invoked by objects from all abstraction levels. Their position in the hierarchy is ambiguous—these are called cross-cutting concerns. Aspect-Oriented Programming (AOP) attempts to solve this through metaprogramming, managing them from a single central point using declarative, policy-based approaches.
The code we write is a model of the real world—not something that reflects reality in its entirety, but something that captures as much as necessary. I mention this because errors are also part of this model, and I want to emphasize that modeling errors you cannot react to is pointless.
Consider an object that communicates with a database. Is it possible for the database to throw an error due to insufficient disk space? Yes, but if we accept that this object cannot do anything about such an issue, modeling this error becomes unnecessary. It’s better for the application to log the error and fail gracefully, allowing human intervention to resolve the underlying problem.
In other words, I’m arguing that it’s unnecessary to model an error like “DiskStorageExhausted” if you cannot recover from it.