CodeGym /Courses /Module 5. Spring /Exception handling in controllers using @ExceptionHandler...

Exception handling in controllers using @ExceptionHandler

Module 5. Spring
Level 7 , Lesson 9
Available

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:

  1. Show the user clear messages instead of "something went wrong".
  2. Centralize error management.
  3. Minimize chaos in controller code.
  4. 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:

  1. The hello method throws an exception if the name parameter is missing.
  2. @ExceptionHandler(IllegalArgumentException.class) catches IllegalArgumentException, 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

  1. Duplicating handling in @ControllerAdvice and controllers. If an exception is handled locally, the global handler won't run.
  2. 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).
  3. Catching the base Exception. If you add a handler for Exception, 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! 🚀

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION