Handling an exception is one of the most common but not necessarily one of the easiest tasks. It is still one of the frequently discussed topics in experienced teams, and there are several best practices and common mistakes you should be aware of.
Here are a few things you should avoid when handling exceptions in your application.
As I explained in one of my previous posts, you either need to specify or handle a checked exception. But checked exceptions are not the only ones you can specify. You can use any subclass of java.lang.Throwable in a throws clause. So, instead of specifying the two different exceptions that are thrown by the following code snippet, you could just use the java.lang.Exception in the throws clause.
public void doNotSpecifyException() throws Exception { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }
But that doesn’t mean that you should do that. Specifying an Exception or Throwable makes it almost impossible to handle them properly when calling your method.
The only information the caller of your method gets is that something might go wrong. But you don’t share any information about the kind of exceptional events that might occur. You’re hiding this information behind an unspecific throws clause.
It gets even worse when your application changes over time. The unspecific throws clause hides all changes to the exceptions that a caller has to expect and handle. That might cause several unexpected errors that you need to find by a test case instead of a compiler error.
It’s, therefore, much better to specify the most specific exception classes even if you have to use multiple of them. That tells the caller of your method which exceptional events need to be handled. It also allows you to update the throws clause when your method throws an additional exception. So your clients are aware of the change and even get an error if you change your throws clause. That is much easier to find and handle than an exception that only shows up when you run a particular test case.
public void specifySpecificExceptions() throws NumberFormatException, IllegalArgumentException { doSomething(); }
The severity of this mistake depends on the kind of software component you’re implementing and where you catch the exception. It might be ok to catch a java.lang.Exception in the main method of your Java SE application. But you should prefer to catch specific exceptions, if you’re implementing a library or if you’re working on deeper layers of your application.
That provides several benefits. It allows you to handle each exception class differently and it prevents you from catching exceptions you didn’t expect.
But keep in mind that the first catch block that handles the exception class or one of its superclasses will catch it. So, make sure to catch the most specific class first. Otherwise, your IDEs will show an error or warning message telling you about an unreachable code block.
try { doSomething(); } catch (NumberFormatException e) { // handle the NumberFormatException log.error(e); } catch (IllegalArgumentException e) { // handle the IllegalArgumentException log.error(e); }
That is one of the most popular mistakes when handling Java exceptions. It might seem logical to log the exception where it was thrown and then rethrow it to the caller who can implement a use case specific handling. But you should not do it for the following three reasons:
So, better only log the exception when you handle it. Like in the following code snippet. The doSomething method throws the exception. The doMore method just specifies it because the developer doesn’t have enough information to handle it. And it then gets handled in the doEvenMore method which also writes a log message.
public void doEvenMore() { try { doMore(); } catch (NumberFormatException e) { // handle the NumberFormatException } catch (IllegalArgumentException e) { // handle the IllegalArgumentException } } public void doMore() throws NumberFormatException, IllegalArgumentException { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }
Using exceptions to control the flow of your application is considered an anti-pattern for two main reasons:
So, better use proper conditions to break your loops or if-else-statements to decide which code blocks should be executed.
You sometimes might want to wrap an exception in a different one. Maybe your team decided to use a custom business exception with error codes and a unified handling. There’s nothing wrong with this approach as long as you don’t remove the cause.
When you instantiate a new exception, you should always set the caught exception as its cause. Otherwise, you lose the message and stack trace that describe the exceptional event that caused your exception. The Exception class and all its subclasses provide several constructor methods which accept the original exception as a parameter and set it as the cause.
try { doSomething(); } catch (NumberFormatException e) { throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR); } catch (IllegalArgumentException e) { throw new MyBusinessException(e, ErrorCode.UNEXPECTED); }
When you generalize an exception, you catch a specific one, like a NumberFormatException, and throw an unspecific java.lang.Exception instead. That is similar to but even worse than the first mistake I described in this post. It not only hides the information about the specific error case on your API, but it also makes it difficult to access.
public void doNotGeneralizeException() throws Exception { try { doSomething(); } catch (NumberFormatException e) { throw new Exception(e); } catch (IllegalArgumentException e) { throw new Exception(e); } }
As you can see in the following code snippet, even if you know which exceptions the method might throw, you can’t simply catch them. You need to catch the generic Exception class and then check the type of its cause. This code is not only cumbersome to implement, but it’s also hard to read. It get’s even worse if you combine this approach with mistake 5. That removes all information about the exceptional event.
try { doNotGeneralizeException(); } catch (Exception e) { if (e.getCause() instanceof NumberFormatException) { log.error("NumberFormatException: " + e); } else if (e.getCause() instanceof IllegalArgumentException) { log.error("IllegalArgumentException: " + e); } else { log.error("Unexpected exception: " + e); } }
So, what’s the better approach?
That’s easy to answer. The exceptions that you throw should always be as specific as possible. And if you wrap an exception, you should also set the original one as the cause so that you don’t lose the stack trace and other information that describe the exceptional event.
try { doSomething(); } catch (NumberFormatException e) { throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR); } catch (IllegalArgumentException e) { throw new MyBusinessException(e, ErrorCode.UNEXPECTED); }
As I explained earlier, it can be useful to wrap exceptions into custom ones as long as you set the original exception as its cause. But some architects overdo it and introduce a custom exception class for each architectural layer. So, they catch an exception in the persistence layer and wrap it into a MyPersistenceException. The business layer catches and wraps it in a MyBusinessException, and this continues until it reaches the API layer or gets handled.
public void persistCustomer(Customer c) throws MyPersistenceException { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer try { persistCustomer(c); } catch (MyPersistenceException e) { throw new MyBusinessException(e, e.getCode()); } } public void createCustomer(Customer c) throws MyApiException { // create a Customer try { manageCustomer(c); } catch (MyBusinessException e) { throw new MyApiException(e, e.getCode()); } }
It’s easy to see that these additional exception classes don’t provide any benefits. They just introduce additional layers that wrap the exception. And while it might be fun to wrap a present in a lot of colorful paper, it’s not a good approach in software development.
Just think about the code that needs to handle the exception or yourself when you need to find the problem that caused the exception. You first need to dig through several layers of exceptions to find the original cause. And until today, I’ve never seen an application that used this approach and added useful information with each exception layer. They either generalize the error message and code, or they provide redundant information.
So, be careful with the number of custom exception classes you introduce. You should always ask yourself if the new exception class provides any additional information or other benefits. In most cases, you don’t need more than one layer of custom exceptions to achieve that.
public void persistCustomer(Customer c) { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer throw new MyBusinessException(e, e.getCode()); } public void createCustomer(Customer c) throws MyBusinessException { // create a Customer manageCustomer(c); }
As you’ve seen, there are several common mistakes you should try to avoid when you handle Java exceptions. That helps you to avoid common bugs and to implement applications that are easy to maintain and to monitor in production.
If this quick list of common mistakes was useful, you should also take a look at my best practices post. It provides you with a list of recommendations that are used by most software development teams to implement their exception handling and to avoid problems like the ones described in this post.
When using Retrace APM with code profiling, you can collect exceptions directly from Java, without any code changes!
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]