CodeGym /Courses /Module 5. Spring /Practice: creating a custom error handler

Practice: creating a custom error handler

Module 5. Spring
Level 8 , Lesson 6
Available

Today we'll build a custom error handler that lets you flexibly adapt exception handling logic to your app's needs. You'll also learn how to create your own exceptions and make sure users see only clear, useful messages instead of weird stack traces.


Why it matters

Errors are a natural part of programming. But how you handle them can have a big impact on user experience and app stability. Imagine a user sees something like:


org.springframework.dao.DataIntegrityViolationException: could not execute statement

They'll probably think: "What the heck? Is my computer broken?!" Custom exception handlers let you turn stuff like that into clear messages, for example:


{
    "error": "Invalid Request",
    "message": "User with the same email already exists"
}

Much better, right?


Step 1: Creating a custom exception

Before we handle errors, let's create our own exception. It should be specific enough so we can easily tell it apart from other error types.

Example: UserNotFoundException


// This is our custom exception
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

Note that this exception extends RuntimeException. That means it's unchecked and doesn't require mandatory handling wherever it might be thrown, which makes it a good fit for most web app cases.

Where to throw the exception?

Say we have a service that works with users:


@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
    }
}

If a user with the given id isn't found, our service will throw UserNotFoundException.


Step 2: Creating a custom error handler

Now we want to catch UserNotFoundException and convert it into a friendlier response for the client (for example, JSON with an error message).

To do that we use a global error handler via @ControllerAdvice.

Custom handler implementation


@RestControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND) // HTTP 404
    public ErrorResponse handleUserNotFoundException(UserNotFoundException ex) {
        return new ErrorResponse("Not Found", ex.getMessage());
    }
}

Helper class ErrorResponse

To return a clear response to the client, let's create a DTO:


public class ErrorResponse {

    private String error;
    private String message;

    public ErrorResponse(String error, String message) {
        this.error = error;
        this.message = message;
    }

    // Getters and setters for serialization
    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Step 3: Testing the custom error handler

Now, to see how this works, let's add a controller:


@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.findUserById(id);
    }
}

Example request:


GET /users/42

If a user with ID 42 is missing, the server will return:


{
    "error": "Not Found",
    "message": "User with id 42 not found"
}

Step 4: Logging errors

It's important not only to return proper responses to the client, but also to log detailed info for developers. Let's add logging to our handler:


@RestControllerAdvice
public class CustomExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);

    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleUserNotFoundException(UserNotFoundException ex) {
        logger.error("User not found exception: {}", ex.getMessage());
        return new ErrorResponse("Not Found", ex.getMessage());
    }
}

Now every time UserNotFoundException occurs, the error will be written to the logs.


Step 5: Handling multiple exceptions

Sometimes you need to handle several exception types at once. Just add more methods to CustomExceptionHandler with different @ExceptionHandler annotations.

Example handling IllegalArgumentException


@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // HTTP 400
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
    return new ErrorResponse("Bad Request", ex.getMessage());
}

Now, if IllegalArgumentException is thrown somewhere in the app, the client will get:


{
    "error": "Bad Request",
    "message": "Invalid input data"
}

Step 6: Testing your exception handlers

To verify your handlers, use tools like Postman or cURL, or write integration tests.

MockMvc test example


@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void shouldReturn404WhenUserNotFound() throws Exception {
        Mockito.when(userService.findUserById(42L))
               .thenThrow(new UserNotFoundException("User with id 42 not found"));

        mockmvc.perform(get("/users/42"))
               .andExpect(status().isNotFound())
               .andExpect(jsonPath("$.error").value("Not Found"))
               .andExpect(jsonPath("$.message").value("User with id 42 not found"));
    }
}

Practical usage

In real projects custom error handlers are used to:

  • Avoid leaking technical details (like stack traces) to the client.
  • Ensure consistency in error handling and response formatting.
  • Clearly separate error handling responsibilities between developers (different controllers can have their own custom handlers).
  • Make it easy to log and track root causes of errors.

Now your app is ready to deal with various exceptions, and users will appreciate friendly messages instead of mysterious "500 Internal Server Error".

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