Typical Exception Handling In Java ☕️
It's a common thing in Java to try-catch parts of our code that we except to fail for some reason
- Missing files, corrupt data, etc...
try{
buggyMethod();
return "Done!";
}catch (RuntimeException e){
return "An error happened!";
}
Exception Handling In Spring 🍃
Let's view the workflow of Spring as a web framework:
- Listen to requests from the client.
- Take some actions based on our business logic.
- Return a response to the client containing the result of our work.
Now, we ideally want to catch any exception (error) that might arise at level 2 (action taking).
We can write a try catch block at every controller method that handles the exceptions in a standard way 🙌🏽
@RestController
@RequiredArgsConstructor
public class TestController
{
private final ExceptionHandler exceptionHandler;
@GetMapping("/test1")
public void test1(){
try{
// test 1 things
}catch (Exception e){
exceptionHandler.handleException(e);
}
}
@GetMapping("/test2")
public void test2(){
try{
// test 2 things
}catch (Exception e){
exceptionHandler.handleException(e);
}
}
}
👎🏽 The problem with this approach however is that it get quite tedious when we have many more controller methods.
Why capture all exceptions? and not just let them occur?🤷🏼
- We want our application to be user friendly and handle all edge cases, thus we want it to return responses with standard format.
- We might also want to log those exceptions in a backlog to get back to them and investigate them, or do whatever we like with them.
@ControllerAdvice To The Rescue💪🏾
The idea is that we declare a method that will handle any unhandled exceptions in the application.
How to do it? 👀
First, we need to declare a class and annotate it with @ControllerAdvice
.
Then, we declare methods, each handling a class of exception.
@ControllerAdvice @Slf4j
public class GlobalErrorHandler
{
@ResponseStatus(INTERNAL_SERVER_ERROR)
@ResponseBody
@ExceptionHandler(Exception.class)
public String methodArgumentNotValidException(Exception ex) {
// you can take actions based on the exception
log.error("An unexpected error has happened", ex);
return "An internal error has happened, please report the incident";
}
@ResponseStatus(BAD_REQUEST)
@ResponseBody
@ExceptionHandler(InvalidParameterException.class)
public String invalidParameterException(InvalidParameterException ex){
return "This is a BAD REQUEST";
}
}
What does the above code do?☝️
- Declares two methods that will be run whenever an exception of class
Exception
,InvalidParameterException
(or subclass of them) is thrown and not handled locally in their thread of execution. - They return a string response back the client.
Note that we can specify more than one handler in the class annotated with @ControllerAdvice
.
Now, let's code some endpoints for us to validate against. Let's code three endpoints
- One that handles the exception thrown.
- The other two leave the handling to the global exception handler
@RestController @RequiredArgsConstructor
public class TestController
{
@GetMapping("/buggyMethod")
public String testMeWithExceptionHandler(){
try{
buggyMethod();
return "Done!";
}catch (RuntimeException e){
return "An error happened!";
}
}
@GetMapping("/potentialBuggyMethod")
public String testMeWithoutExceptionHandler(){
undercoverBuggyMethod();
return "Done!";
}
@PostMapping("/invalidParamMethod")
public String testForInvalidParam(){
buggyParameters();
return "Done";
}
private void buggyMethod(){
throw new RuntimeException();
}
private void undercoverBuggyMethod(){
throw new RuntimeException("oops");
}
private void buggyParameters(){
throw new InvalidParameterException();
}
}
Let's Verify It With Some Tests 🧠
@WebMvcTest(controllers = TestController.class)
public class GlobalExceptionHandlerTest
{
@Autowired
private MockMvc mockMvc;
@Test
public void givenAGetRequestToBuggyEndPoint_DetectErrorMessage() throws Exception
{
MvcResult mvcResult = mockMvc
.perform(get("/buggyMethod"))
.andExpect(status().isOk())
.andReturn();
String response = mvcResult.getResponse().getContentAsString();
assertEquals(response, "An error happened!");
}
@Test
public void givenAGetRequestToPotentialBuggyMethod_DetectErrorMessage() throws Exception
{
MvcResult mvcResult = mockMvc
.perform(get("/potentialBuggyMethod"))
.andExpect(status().is5xxServerError())
.andReturn();
String response = mvcResult.getResponse().getContentAsString();
assertEquals(response, "An internal error has happened, please report the incident");
}
@Test
public void givenAPostRequestToBuggyMethod_DetectInvalidParameterErrorMessage() throws Exception
{
MvcResult mvcResult = mockMvc
.perform(post("/invalidParamMethod"))
.andExpect(status().isBadRequest())
.andReturn();
String response = mvcResult.getResponse().getContentAsString();
assertEquals(response, "This is a BAD REQUEST");
}
}
Conclusion 👈
Unexpected and general errors should be handled elegantly to sustain a smooth experience for our application clients. This is best done using Spring's ControllerAdvice.
Top comments (10)
This is a way to do it. The best? Probably not. First spring typically handles most errors in a generic way that is good enough for almost all clients. Most clients are only going to care about failure or security or success. If not the actual best way to do it is to throw a custom exception and annotate that exception so it will throw the desired HTTP code. That way you have a custom exception which is the right thing to do and then handle them in a specific way versus a somewhat less generic way via the controller advice
I agree for internal facing applications. For external you don't want the stacktrace to be sent to the client since it allows would be hackers to gain insight into the inner workings of your application.
See that's the thing about springboat there's always a way to handle that via configuration. There is a one line configuration to turn that off and you can do it by environment with profiles
Setting HTTP code for you own exceptions - is the best way to high coupled monolith with mixed everything together. "The best? Probably not." )
Thanks for the feedback :) yeah you are right, however I've found that controller advice can also be like a final fishnet for any unexpected events.
Yeah I understand what you're saying but I've got about 40 spring boot applications and I've never needed it. The default spring boot mechanism handles the majority of unexpected events.
I have used it :D so it really depends on your work style
Well it's not really work style it's really about if you're using spring boot techniques or Reinventing the wheel. But I mean if that's what you want to do go for it LOL
Using of @ControllerAdvice as an idea - it's ok. But the problem of Spring' implementation is that there could be only one @ControllerAdvice with @ExceptionHandler ((. So, this class become really huge (from the responsibility point view, of course).
yes that is certainly a downside