Let’s talk about reporting errors in software. Suppose we write a library used by other programmers. What should happen when our code fails?
What answers could I get if I asked this question during a meetup?
- it should display an error
- it should log the error
- it should throw an exception
- it should return a value indicating an error
All those answers are more or less correct.
Logging an error is essential.
We should throw an exception or state which part of the code encountered a problem. Exceptions have stack traces, so we get (some part of) tracking for free. We know the location in the code, but we still don’t see the state of the process.
We can also return a status code. In this case, I hope you use a return type equivalent to the
Either type in Scala. This type can contain one of two values - either a returned value or an error description. It’s the only valid method of returning status codes! If your function returns the correct value or null, in case, of errors, you are awaiting a disaster.
Logging and returning an error
Log-and-throw is an antipattern because we get the same error message in two places. However, it’s the lesser evil.
When your code detects a problem, it has access to all the context information related to the issue. You can log as much as you want and throw the exception. I won’t argue with you if you use the log-and-throw method because you want to log as much of the application state as possible.
Of course, we can also pass the context data to the exception object.
Describing the error
I recommend storing the context information in the exception object. Such a solution isn’t popular because it requires creating a custom hierarchy of exceptions.
You should do that!
If you can easily distinguish between multiple kinds of errors, create custom exception classes. Don’t use a generic exception class. A generic exception is sufficient for debugging. However, no automation will run reliably if it has to fish out the error from an exception message. You should distinguish them by types, not the error content.
Often, I see people creating a single custom exception class for their entire application and using them for all errors. If they named the application Foo, they write an exception class FooException. What’s the point? Isn’t every exception thrown by Foo a Foo exception by definition?
Creating an exception class with the application name is the most useless thing you can do. Exception names should mean something.
Also, you don’t need to suffix the class name with Exception or Error (unless the naming convention in your programming language forces you to use a suffix). We know it is an exception because it inherits from an exception class.
The worst thing you can do
What is the worst thing you can do?
The worst mistake you can make is using a wrong exception or having a wrong message in the exception. People waste a lot of time when you give them a misleading error.
Of course, throwing a wrong exception is an obvious kind of error. However, we can get the same result - a wrong exception - even if we do nothing.
Say your application inserts data into a database. The database has constraints, and the data you inserted gets rejected. If you use an ORM, it’ll probably throw a valid exception - constraint violation. If you handle database connections on your own, you must take care of throwing a valid exception yourself.
How am I supposed to know I passed invalid input when you throw a generic database error? What if the programmer assumed that every database-related problem was a connection error? We would get a DatabaseConnectionError when we violate the constraint. Would it help us debug an issue caused by invalid values?
Don’t say it won’t happen. I have seen it way too many times.
How to avoid misleading errors?
You may think that nobody would see a misleading error if you didn’t write misleading code. Unfortunately, it’s not true.
If you weren’t intentional about the kind of errors you report, you risk throwing a misinforming exception.
What can you do? Every time you write code, you should think about how it can fail and how you know whether it failed. You should write a test verifying whether your code throws the correct exception.
Exceptions are critical parts of your API. You should test whether you throw the correct one!