Proper Java Exception Handling
Read this helpful article on handling exceptions correctly for peace of mind and to benefit your colleagues. Not only make you happy but also your colleagues.
Join the DZone community and get the full member experience.
Join For FreeLet's talk about sore points. Due to my duties, I have to work with many different services (make edits, conduct code reviews ...); different teams usually write all these services, and whenever it comes to handling errors and forwarding them from the service, sometimes my eyes start watering. Let me try to tell you what code, in my opinion, is not acceptable for error handling and how it should be in my opinion.
In general, initially, the problem is hidden in the flaw in the analytics of the service. Often, there is no requirement in terms of reference on how errors should be thrown. As a rule, this happens for two reasons; the first is the rush to develop a new service, and the second is that the analyst trusts the experience of the developer. In such cases, the analyst simply tells the developer, "Well, give me an example of an error message later, and I will attach it in confluence."
Let's move on to examples; let's see the consequences of this approach in development.
The first thing not to do is just throw a RuntimeException:
public Object badExample() {
throw new RuntimeException("Something wrong!");
}
Display:
The calling service or client will receive a 500 error and will not understand what is wrong with its requests. How long do you think it takes to find the problem in such cases? It will grow in proportion to the amount of your code. And if you have a method call chain, then it becomes more difficult to process such messages in calling methods inside the service.
How to improve it? First, let's create an error handler; almost all frameworks have it out of the box; in my examples, I will use the Spring framework handler.
Example:
@ControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = {RuntimeException.class})
public ResponseEntity<Object> handler(RuntimeException e) {
HttpStatus badRequest = HttpStatus.INTERNAL_SERVER_ERROR;
return new ResponseEntity<>(e.getMessage(), badRequest);
}
...
Display:
Now we see the message, but the format of the message is a string, not JSON. We'll figure that out soon.
Ideally, all services in a project should have the same error message format. At least two fields are the message itself and an internal integer error code. I suppose it is unnecessary to tell why the message text is insufficient and how many resources are required to process the string.
Example:
public class ExampleApiError {
private String message;
private Integer code;
private LocalDateTime dateTime;
...
We will fill this class in the error handler. But as I said, throwing a RuntimeException is bad practice, so you need to create your own class for throwing errors, in the constructor of which we will pass a message and an error code.
public class ApiException extends RuntimeException {
private final int code;
public ApiException(String msg, int code) {
super(msg);
this.code = code;
}
...
It seems that everything is clear further, but even here, problems begin; everyone creates parameters for passing to the class constructor in different ways. Some create a message and an error code directly in the method.
Example:
public Object badExample() {
throw new ApiException("Something wrong!", 10);
}
Finding such places in the code is still difficult. Well, the first thing that comes to mind is to create classes of constants, one class for messages and the second for message codes.
Example:
public static final String SOMETHING_WRONG = "Something wrong";
public static final int SOMETHING_WRONG_CODE = 10;
public Object badExample() {
throw new ApiException(SOMETHING_WRONG, SOMETHING_WRONG_CODE);
}
This is already much better, more readable, and easy to find when you know where the error codes are.
But storing everything in one class can be a bad idea if you don't have a microservice; messages start to get bigger and bigger, so it's better to separate the constant classes according to the functionality of the methods where they will be used with something like ProductExceptionConstant, PaymentExceptionConstant and so on.
But that's not all; it may seem old-fashioned to some, but constants need to be created as Enum or Interface. Variables in an interface are static, public, and final by default. I am not against this approach; the main thing is that everything should be uniform; if you started doing it through interfaces, then continue like that; there is no need to mix. In one of the projects, I saw the same team use three different approaches with constants. You don't have to do that.)
I will show one more example from real projects that caught my eye during the review.
Firstly, the developer decided that all the entities that he returns will contain an error message and an error code, and the status will always be 200, which, in my opinion, is misleading to the caller. Well, an example of constants.
public enum ErrorsEnum {
DEFAULT_ERROR(ErrorsConstants.DEFAULT_ERROR, "400"),
REFRESH_TOKEN_NOT_VALID(ErrorsConstants.REFRESH_TOKEN_NOT_VALID, "400.1"),
USER_NOT_FOUND(ErrorsConstants.USER_NOT_FOUND, "400.2"),
...
It seems to me that the code 9 3 \ 4 is missing. And by default, in this case, you can use the number Pi.
If you are interested in my approach, then go ahead. The most convenient thing is to create an interface that will be a binding contract for everyone.
public interface ExceptionBase {
String getMsg();
Integer getCode();
}
And you need to change the constructor of the exception class.
public ApiException(ExceptionBase e) {
super(e.getMsg());
this.code = e.getCode();
}
Now everyone who will try to throw an exception will understand that we must pass an object that implements our interface to the constructor. And the most suitable option is Enum.
Example:
/**
* This class is intended for declaring exceptions that occur during order processing. code range
* 101-199.
*/
public enum OrderException implements ExceptionBase {
ORDER_NOT_FOUND("Order not found.", 101),
ORDER_CANNOT_BE_UPDATED("Order cannot be updated,", 102);
OrderException(String msg, Integer code) {
this.msg = msg;
this.code = code;
}
private final String msg;
private final Integer code;
@Override
public String getMsg() {
return msg;
}
@Override
public Integer getCode() {
return code;
}
}
Usage Example:
public Object getProduct(String id) {
if (id.equals("-1")) {
throw new ApiException(OrderException.ORDER_NOT_FOUND);
}
...
Server Response:
As a result, we have the following exception package model.
As you can see, nothing complicated; the logic is easy to read. In this example, I left a constructor in the ApiException class that takes a string, but for reliability, it is better to remove it.
Typically, most inconsistencies in code are due to a lack of code checks or weak checks. The most common excuse is, "This is a temporary solution; we'll fix it later," but no, it doesn't work like that; no one will bother to look for where there was a temporary solution and where there was a permanent one. And it turns out that "what is temporary is permanent."
If you have a lot of services that communicate with each other, then by making one error message format, you will greatly simplify the job of writing client libraries. For example, when using Retrofit, once you write the handler core, you only need to change the methods in the interface and the objects you receive.
Conclusion
Error handling is a very important part of the code; it makes it easy to find problem areas in the code and also allows external clients to understand what they are doing wrong when using your endpoints, so you should pay due attention to this from the very first stages of writing a project.
An example code is here.
I hope your life after this article will become a little easier. All success.
Opinions expressed by DZone contributors are their own.
Comments