Exception Handling in Spring Boot

Introduction

In this tutorial, we will see how to do Exception Handling in Spring Boot REST API that we build in the previous modules.

Exception handling is an important aspect of any application, including Spring Boot applications. When an unexpected error or exception occurs in a Spring Boot application, it can cause the application to crash or behave unexpectedly. Therefore, it is important to handle exceptions properly to provide a better user experience and ensure the stability of the application.

In Spring Boot, you can handle exceptions by using the @ExceptionHandler annotation. This annotation is used to define a method that can handle a specific exception type. When an exception occurs in the application, Spring Boot will look for a matching @ExceptionHandler method and execute it.

Handling Exception in Spring Boot Application

Here are the steps to handle exceptions in our Spring Boot Bookstore REST API:

Create custom exception classes

Create a custom exception class to represent the error scenario. For example, we can create a BookNotFoundException class to handle the case when a book is not found.

package com.notearena.bookstore.exception;

/*
 * ------------------------------------------------------|
 * Spring Boot REST API
 * =====================================
 * 2023 NoteArena.com opensource project
 * @Author: Mamun Kayum
 * @Modified:
 * @Version: 1.0
 * Feel free use this implementation
 *-------------------------------------------------------|
 */
public class BookNotFoundException extends RuntimeException {
    public BookNotFoundException() {
        super();
    }

    public BookNotFoundException(String message) {
        super(message);
    }

    public BookNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

Next, we can create a BookAlreadyExistsException class to handle the case when a book already exists.

package com.notearena.bookstore.exception;

/*
* ------------------------------------------------------|
* Spring boot REST API
* =====================================
* 2023 NoteArena.com opensource project
* @Author: Mamun Kayum
* @Modified:
* @Version: 1.0
* Feel free use this implementation
*-------------------------------------------------------|
*/
public class BookAlreadyExistsException extends RuntimeException{
public BookAlreadyExistsException() {
super();
}

public BookAlreadyExistsException(String message) {
super(message);
}
}

Create a new class called ErrorResponse

Create a new class called ErrorResponse under the com.notearena.bookstore.exception package. This class will be used to send an error response back to the client.

package com.notearena.bookstore.exception;

import org.springframework.http.HttpStatus;

/*
* ------------------------------------------------------|
* Spring Boot REST API
* =====================================
* 2023 NoteArena.com opensource project
* @Author: Mamun Kayum
* @Modified:
* @Version: 1.0
* Feel free use this implementation
*-------------------------------------------------------|
*/
public class ErrorResponse {
private HttpStatus status;
private String message;
private long timestamp;

public ErrorResponse(HttpStatus status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}

// getters and

public HttpStatus getStatus() {
return status;
}

public void setStatus(HttpStatus status) {
this.status = status;
}

public String getMessage() {
return message;
}

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

public long getTimestamp() {
return timestamp;
}

public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}

Create a custom exception handler class

Next, we need to add the custom exception handler to the application. We can do this by creating a class that implements the ResponseEntityExceptionHandler class and annotating it with @ControllerAdvice. This will ensure that the exception handling applies to all controllers in the application. Here is the example implementation:

package com.notearena.bookstore.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

/*
 * ------------------------------------------------------|
 * Spring boot REST API
 * =====================================
 * 2023 NoteArena.com opensource project
 * @Author: Mamun Kayum
 * @Modified:
 * @Version: 1.0
 * Feel free use this implementation
 *-------------------------------------------------------|
 */
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage(), System.currentTimeMillis());
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(BookNotFoundException.class)
    public ResponseEntity<Object> handleBookNotFoundException(BookNotFoundException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage(), System.currentTimeMillis());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler({ BookAlreadyExistsException.class })
    protected ResponseEntity<Object> handleBookAlreadyExists(
            Exception ex, WebRequest request) {
        String message = "Book already exists";
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.CONFLICT, ex.getMessage(), System.currentTimeMillis());
        return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
    }

    @ExceptionHandler(InvalidRequestException.class)
    public ResponseEntity<Object> handleInvalidRequestException(InvalidRequestException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), System.currentTimeMillis());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

In this class, we define the exception handlers for each exception we want to handle. In this example, we have defined handlers for BookNotFoundException, BookAlreadyExistsException, InvalidRequestException and Exception.

The first method, handleBookNotFoundException is annotated with @ExceptionHandler(BookNotFoundException.class) to indicate that it will handle BookNotFoundException. Inside the method, we create a new ErrorResponse object with the appropriate status code, error message, and timestamp. We then return a new ResponseEntity object with the error response and HttpStatus.NOT_FOUND.

handleBookAlreadyExists method handles the BookAlreadyExistsException and returns a 409 Conflict status code with a custom error message and timestamp.

handleAllExceptions method handles all other exceptions and returns a 500 Internal Server Error status code with the exception message and timestamp.

Finally, we define a handleInvalidRequestException method that is to return an exception message whenever we will have an invalid request.

In the controller class, we can throw the BookNotFoundException and other exceptions whenever a book is not found, a book already exists, and many more.

Updated BookController class with Exception Handling

We will modify the BookController class to throw a ResourceNotFoundException when the requested book resource is not found. Likewise, we will throw other exceptions based on the functions of the methods.

package com.notearena.bookstore.controller;

import com.notearena.bookstore.exception.BookAlreadyExistsException;
import com.notearena.bookstore.exception.BookNotFoundException;
import com.notearena.bookstore.model.Book;
import com.notearena.bookstore.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

/*
 * ------------------------------------------------------|
 * Spring Boot REST API
 * =====================================
 * 2023 NoteArena.com opensource project
 * @Author: Mamun Kayum
 * @Modified:
 * @Version: 1.0
 * Feel free use this implementation
 *-------------------------------------------------------|
 */

@RestController
@RequestMapping("/books")
public class BookController {

    private final BookRepository bookRepository;

    @Autowired
    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        List<Book> books = bookRepository.findAll();
        return ResponseEntity.ok(books);
    }

    @GetMapping("/{id}")
    public Book getBookById(@PathVariable Long id) {
        Optional<Book> book = bookRepository.findById(id);
        return book.orElseThrow(() -> new BookNotFoundException("Book not found with id: " + id));
    }

    @PostMapping
    public ResponseEntity<Void> createBook(@RequestBody Book book) {
        bookRepository.findByTitleAndAuthor(book).ifPresent(book1 -> {throw new BookAlreadyExistsException("Book already exists!");});
        bookRepository.save(book);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable("id") Long id, @RequestBody Book book) {
        Book existingBook = bookRepository.findById(id)
                .orElseThrow(() -> new BookNotFoundException("Book not found with id " + id));
        existingBook.setTitle(book.getTitle());
        existingBook.setAuthor(book.getAuthor());
        Book updatedBook = bookRepository.update(existingBook.getId(),existingBook);
        return new ResponseEntity<>(updatedBook, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable("id") Long id) {
        Book book = bookRepository.findById(id)
                .orElseThrow(() -> new BookNotFoundException("Book not found with id " + id));
        bookRepository.delete(book.getId());
        return ResponseEntity.noContent().build();
    }

}

When getBookById method is called with an invalid book ID, a ResourceNotFoundException will be thrown, and the global exception handler will be invoked to return an appropriate error response.

Here, we call the orElseThrow method on the Optional<Book> returned by findById. This method takes a Supplier argument that returns an exception to be thrown if the optional is empty. In this case, we create and throw a new BookNotFoundException with the given id.

Testing the API

Now that we have our data custom exception class, exception handler, and controller defined, let’s test our API using Postman.

1. Open Postman and create a new request.

2. Create a new request and set the request method to POST and the request URL to http://localhost:8080/books.

In the request body, enter JSON data for a new book. For example:

{ "title": "Building a REST API", "author": "Mamun Kayum" }
add book

Now if we send the same JSON data for the book, it will return us response with an exception message:

Exception Handling in Spring Boot

Create a new request and set the request method to GET and the request URL to http://localhost:8080/books/{id}, where {id} is the id of a book in our data store. Use an id that is not available in our data store.

Click the Send button to make the request. You should see a response with an error message that the book is not found.

Now, if we want to delete a book that is not available, we will see an error response:

delete book

Conclusion

By using the @ExceptionHandler annotation to handle exceptions in our Spring Boot REST API, we can ensure that our API returns meaningful error messages and status codes to clients, improving the overall user experience.

Visit our springboot-projects GitHub repository for the code.

Also, see the example code JavaExamples_NoteArena in our GitHub repository. See complete examples in our GitHub repositories.

Follow us on social media
Follow Author