Let's kick off with a simple question: what happens if your app runs into something unexpected? For example, a user requests a resource that doesn't exist, or types a string where a number was expected. Without proper handling, your server will proudly spit out a colorful stack trace with a 500 error in the browser, which, sadly, won't make your user happy.
Exception handling helps to:
- Show the user clear messages instead of "something went wrong".
- Centralize error management.
- Minimize chaos in controller code.
- Ensure security (for example, avoid leaking technical details).
@ExceptionHandler: your local error-handling superhero
Spring MVC provides the @ExceptionHandler annotation, which lets you handle exceptions that occur in controllers. Here's how it works.
Simple error handling
@Controller
public class DemoController {
@GetMapping("/hello")
public String hello(@RequestParam(required = false) String name) {
if (name == null) {
throw new IllegalArgumentException("Name is required!");
}
return "Hello, " + name;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "Error: " + ex.getMessage();
}
}
Step by step:
- The
hellomethod throws an exception if thenameparameter is missing. @ExceptionHandler(IllegalArgumentException.class)catchesIllegalArgumentException, and instead of the standard stack trace the user gets a readable message.
Try calling /hello without the name parameter. You'll see: Error: Name is required!.
Handling multiple exception types
If your controller can throw different exceptions, you can handle each one separately.
@Controller
public class MultiExceptionController {
@GetMapping("/data/{id}")
public String getData(@PathVariable("id") int id) {
if (id < 0) {
throw new IllegalArgumentException("ID cannot be negative");
} else if (id == 0) {
throw new NullPointerException("ID cannot be zero");
}
return "Data for ID " + id;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgument(IllegalArgumentException ex) {
return "Invalid argument: " + ex.getMessage();
}
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public String handleNullPointer(NullPointerException ex) {
return "Error: " + ex.getMessage();
}
}
Now we can configure different responses for different error types:
- If the user requests
/data/-1, they'll see:Invalid argument: ID cannot be negative. - If they request
/data/0, they'll see:Error: ID cannot be zero.
Centralizing error handling: @ControllerAdvice
If you have more than a couple controllers, it's a good idea to move common exception handling to a separate class. Use the @ControllerAdvice annotation for that.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "Global error: " + ex.getMessage();
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleAllExceptions(Exception ex) {
return "Something went wrong: " + ex.getMessage();
}
}
Now IllegalArgumentException and any other exceptions thrown in your app will be handled centrally. You can keep local handlers in controllers for specific cases or remove them altogether.
How to return nice responses: JSON instead of plain text
In real apps we often return responses in JSON format rather than plain text. This is especially important for REST APIs.
Example with JSON responses
@ControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", "Invalid argument");
response.put("message", ex.getMessage());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}
Now, requesting /hello without the name parameter will give you the following JSON response:
{
"error": "Invalid argument",
"message": "Name is required!"
}
Using ResponseEntity we can also set the HTTP status (for example, 400 - Bad Request).
Handling ValidationException specifics
If you use data validation with @Valid, you also need to handle the exceptions it throws. For example:
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String addUser(@Valid @RequestBody User user) {
return "User " + user.getName() + " added!";
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
public class User {
@NotNull(message = "Name cannot be empty")
private String name;
@Email(message = "Invalid email")
private String email;
// getters and setters
}
If you send a request without a name or with an invalid email, the response will be a JSON describing all validation errors.
How to properly test exception handling
To test your handlers you can use MockMvc. Here's how:
@WebMvcTest(MultiExceptionController.class)
public class ExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testHandleIllegalArgument() throws Exception {
mockMvc.perform(get("/data/-1"))
.andExpect(status().isBadRequest())
.andExpect(content().string("Invalid argument: ID cannot be negative"));
}
@Test
public void testHandleNullPointer() throws Exception {
mockMvc.perform(get("/data/0"))
.andExpect(status().isInternalServerError())
.andExpect(content().string("Error: ID cannot be zero"));
}
}
MockMvc lets you verify that exceptions are handled correctly and the expected responses are returned.
Common mistakes when handling exceptions
- Duplicating handling in
@ControllerAdviceand controllers. If an exception is handled locally, the global handler won't run. - Not setting the HTTP status. If you return JSON, don't forget to set the proper HTTP status (for example, 400 for a bad request).
- Catching the base
Exception. If you add a handler forException, it will mask all specific exception handlers. Use this carefully.
Now you know how to properly handle exceptions in Spring MVC! Next time, instead of the "red horror" stack trace the user will see a friendly message and stay happy. Good luck with your coding! 🚀
GO TO FULL VERSION