One of the common topics you always see in programming articles is the one about proper use of exception handling and the try/catch mechanism (if supported by your language of choice). As you absorb the wisdom of the sacred text, looking for that nugget of information about how to do it properly, you almost always come across the idea of abstraction. The incantation often reads something like “Throw exceptions at the proper level of abstraction that makes sense.” What do they mean by that? How do I know if I am throwing at the correct level? What do they even mean by level? I will break it down for you, bit by bit, in this article.
To help illustrate what abstractions are and what other people might mean by levels, I am going to talk about a person. A person is a complex system made up of other systems like the digestive system, circulatory system, endocrine system, nervous system etc. When you talk about a person (in the general sense) you are talking about an abstraction. The person is a “high level view or concept” of all those internal systems working together. You don’t deal with a person’s circulatory system, you deal with a person as a whole. A person is a concept.
Let’s say that we were a surgeon who specializes in the circulatory system of the human body (maybe a cardiologist). When we are operating on a person, we are focused on fixing the blood related problems that a person may have. The circulatory system is an abstraction or “high level view” of the heart, veins, arteries and capillaries. If the lungs want to deliver oxygen to the body, it has to interact or “interface” with this idea of the circulatory system.
If a person is an abstraction of their body’s subsystems, a subsystem is an abstraction of its subsystems and on and on we go. These form the levels. Our world is full of abstractions because it helps us think about the world. We can deal in the abstraction, talking to a person, without knowing how their circulatory system works.
Well, the human body has its issues. We get sick, our bodies break down to aging or something in one system goes wrong and it can make us feel like garbage. Imagine that you were talking to your friend and suddenly they yelled out “Stomach acid overflow detected, aborting!” You would probably be like “Wtf?” This is a scenario where an error in the digestive system throws an exception and it is not caught until it reaches your ears. The proper level of abstraction would have suggested that instead the digestive system throws the exception and it is caught by the person who owns the stomach where it is translated to “Heartburn”. Your friend then says, in a more friendly message, “Oh I think that burger didn’t agree with me. I have a bit of heartburn.”
We in the outside world should not be talking to your stomach, we talk to the person. The person is what “abstracts away” the functions of the digestive system. It does us no good to know of a stomach acid overflow because what does that mean to us or how are we suppose to realize that is heartburn. Imagine a single cell dies. Does it need to throw that fact up all the way to the person? Can you imagine knowing about every cell in your body when it dies?
Taking the same concept, and providing an example in software, imagine you have a class that is responsible for interacting with a website. It opens a connection, sends and receives data and closes the connection. It has many moving parts or subsystems. As an abstraction, you deal with the class WebsiteConnection, you don’t care about the underlying parts of how it sends and retrieves the data. Much like our example of the stomach, if the connection fails we don’t throw a raw exception up to say “opening TCP connection failed”. We may not even know the class ueses a tcp connection, nor do we even care. All we care about is if the class is successful in reading the website. The dead give away that this is the problem is when you read the error and say “Where do I even setup a TCP connection?” because it appears to you that you are only dealing with a WebsiteConnection and mention nothing of TCP.
The problem with not throwing exceptions on the right level is that it leaks information that we may not even be aware of. In our class example, it just leaked that it opens the connection with a TCP connection. We as developers may not mind this, but imagine that someone is using our image editing app and suddenly they get an error message pop up saying “opening TCP connection failed”. They will be like “What does that mean?”
Not only are we unaware, we are now forced to know all the inner details of our class and the exceptions it may throw. Should we be looking to catch a TCP specific exception when dealing with a WebsiteConnection class? What about IOException? I don’t know, I would have to look at the source code of the class or read the docs. I hope you get my point.
One rule you may see people mention is that you should not try to catch an exception if you don’t know how to handle it. In general this is very good advice. The one exception (pun not intended) to this is in the case of levels of abstraction. It is perfectly acceptable to catch that TCP connection failed exception only to wrap it in a new exception that makes more sense at the higher level. For example, you catch TCP connection failed inside your class but then you wrap it in a new exception called “ReadConnectionException” and has a message of “Unable to read data from the website. Maybe it is down.” and throw that. If an exception like that bubbles up to the user at least it will make sense. It will be a message at a higher level of understanding where the user is not being exposed to the inner workings of network error messages.
Let’s go back to our Person/DigestiveSystem analogy and take a look at a generic example where we have two classes. Person makes use of the DigestiveSystem class. Let’s try and apply our new idea of escalating exceptions at the proper level.
<?php class DigestiveSystem { public function Digest($food) { // Something goes wrong throw new DigestionException("Stomach acid overflow! Aborting!"); } } class Person { protected $name; protected $age; protected $digestiveSystem; public function __construct($name, $age) { $this->name = $name; $this->age = $age; $this->digestiveSystem = new DigestiveSystem(); } public function eat($food) { // This will throw the exception, but since we don't catch it, it bubbles up out of the person class. // Wrong level of abstraction! $this->digestiveSystem->digest($food); } }
Here is one way we could write the eat() method to better utilize the correct level of abstraction from DigestiveSystem to Person…
public function eat($food) { // This time we catch the exception at this level and even though we can't do anything about it now, // we will wrap it up and throw it with the correct level of abstraction. // This way any user of the Person class sees a message and exception that makes sense! try { $this->digestiveSystem->digest($food); } catch (DigestionException $e) { // Notice we include $e in the exception so we have the stack trace // Also notice that this new exception makes more sense coming from a person. // We could also take this opportunity to do something internally with $e (like log it). throw new HeartBurnException("Whoa, $food doesn't agree with me! I have heartburn!", 0, $e); } }
As you can see from the code and comments above, we don’t let the DigestiveSystem exception bubble its way out of the Person class. We capture it, re-wrap it and throw it up but this time in a more correct context. A person can have heart burn, their stomach doesn’t tell you that stomach acid is overflowing. Now don’t abuse this with all your exceptions. Too much wrapping can lead to complexity and make things less readable. Use in moderation! Also, if you do use it, make sure to ALWAYS put the inner exception so you don’t lose your stack tracing!
In this article we talked about the idea of an abstractions or “high level view/concept” of one or more subsystems/components. A person is an abstraction of its various internal subsystems. A car is an abstraction of an engine, tires, steering wheel etc. An engine is an abstraction of its pistons, spark plugs, timing belts etc. We talked about how a level of abstraction is how deep into the systems we view. A person is one level of abstraction, a digestive system is another, a stomach is another level etc.
After that we translated the concepts over to software and code design. We talked about how a sub system of a class should not be shouting its issues directly outside the class. If you find yourself trying to catch a TCP exception for a class called “WebsiteConnection” then you should see a red flag because you would have to know that WebsiteConnection is using a TCP connection to start to know it is going to possibly throw that kind of exception.
We revisited our earlier example and provided one quick code example of how we could properly throw exceptions at the right level. We captured the internal exception, re-wrapped it and threw it with a more correct context.
If you make sure that the types of exceptions that make their way up from the specific method, to class, to component to system and on to the user are at the proper levels of abstraction, the exception should always make sense. If you see an exception that appears to be cryptic on an abstraction that is general in nature, be reminded of my example. You don’t care to know about a person’s stomach, you care to know about the person.
Thanks for reading! 🙂