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.