Skip to main content

Exception Handling and Custom Exceptions

Overview​

In this lab, you will:

  • Create custom exceptions
  • Add a generic exception handler
  • Return appropriate status codes with errors

You will use project from the previous lab springboot-rest-demo.

Exception Package​

Folder Setup:​

  • Create a new folder called Exception

ItemNotFoundException:​

  • Create a new class called ItemNotFoundException inside this folder that extends RuntimeException.
  • Add a single constructor that takes a String param and then call the super constructor and pass in this value.
  • This has now created a custom exception that we can throw in the Service class if findById returns null / no item (an Item with that Id isn't in the Database).
Click here to view the example solution
public class ItemNotFoundException extends RuntimeException {

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

InvalidParamsException:​

  • Create a new class called InvalidParamsException inside the same folder.
  • Extend RuntimeException again.
  • Add a single constructor that takes a String param and then call the super constructor and pass in this value.
  • This has now created a second exception that we can throw in the Controller if a request to the API is missing a required value.
Click here to view the example solution
public class InvalidParamsException extends RuntimeException {

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

GlobalExceptionHandler:​

Finally add a class called GlobalExceptionHandler in the same folder.

  • Annotate it with @ControllerAdvice This annotation means that this handler is available for all controllers. It can also be amended with a specific controller name to only be visible by a single controller.

  • Add a handleItemNotFoundException that takes an ItemNotFoundException as a param.

    • It should return a ResponseEntity with a status of HttpStatus.NOT_FOUND) and a body which is the exception.getMessage().
  • Annotate the function with:

    • @ExceptionHandler(ItemNotFoundException.class)
    • @ResponseBody
  • Add a handleGenericException that takes an Exception as a param.

    • It should return a ResponseEntity with a status of HttpStatus.INTERNAL_SERVER_ERROR) and a body which is the exception.getMessage().
  • Annotate the function with

    • @ExceptionHandler(Exception.class)
    • @ResponseBody
  • Add a handleInvalidParamsException that takes an InvalidParamsException as a param.

    • It should return a ResponseEntity with a status of HttpStatus.BAD_REQUEST) and a body which is the exception.getMessage().
  • Annotate the function with

    • @ExceptionHandler(InvalidParamsException.class)
    • @ResponseBody
Click here to view the example solution
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.bind.annotation.ResponseBody;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ItemNotFoundException.class)
@ResponseBody
public ResponseEntity<String> handleItemNotFoundException(ItemNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}

@ExceptionHandler(InvalidParamsException.class)
@ResponseBody
public ResponseEntity<String> handleInvalidParamsException(InvalidParamsException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}

@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<String> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred: " + ex.getMessage());
}
}

ItemService Updates​

Now the functions in the service class need to be updated.

  • Update the getItemById function to throw ItemNotFoundException if no item can be found with that Id.
  • Update the update function to do the same.
  • Update the delete function to check if the item is in the database before deleting and throw ItemNotFoundException if it is not.
Click here to view the example solution
@Service
public class ItemService {

private final ItemRepository itemRepository;
private final Logger logger = LoggerFactory.getLogger(ItemService.class);

public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}

public List<Item> getAllItems() {
return itemRepository.findAll();
}

public Item getItemById(Long id) {
return itemRepository.findById(id)
.orElseThrow(() -> new ItemNotFoundException("Item with id " + id + " not found"));
}

public Item createItem(Item item) {
return itemRepository.save(item);
}

public Item updateItem(Long id, Item updatedItem) {
return itemRepository.findById(id)
.map(existingItem -> {
existingItem.setName(updatedItem.getName());
existingItem.setPrice(updatedItem.getPrice());
return itemRepository.save(existingItem);
})
.orElseThrow(() -> new ItemNotFoundException("Item with id " + id + " not found"));
}

public void deleteItem(Long id) {
if (!itemRepository.existsById(id)) {
throw new ItemNotFoundException("Item with id " + id + " not found");
}
itemRepository.deleteById(id);
}
}

ItemController Updates​

Now the functions in the ItemController need to be updated.

  • Update getItemById to throw InvalidParamsException if no id param is found.
  • Update createItem to throw InvalidParamsException if any values are missing from the request body.
  • Update updateItem to throw InvalidParamsException if the pathVariable id or request body is missing.
  • Update deleteItem to throw InvalidParamsException if the id pathVariable is missing.

Now the controller should return a more controlled exception response if something goes wrong. You can test this by sending a post request to add a new Item to the DB and removing either name or price. You could also send in empty brackets. This will also work for getItem/id and calling it with no id or a 0 (no item will have an id of 0 in the DB.

Click here to view the example solution
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.neueda.entity.Item;
import com.neueda.exception.InvalidParamsException;
import com.neueda.service.ItemService;

@RestController
@RequestMapping("/items")
public class ItemController {

private final ItemService itemService;
private final Logger logger = LoggerFactory.getLogger(ItemController.class);

public ItemController(ItemService itemService) {
this.itemService = itemService;
}

@GetMapping
public ResponseEntity<List<Item>> getItems() {
logger.info("GET /items called");
List<Item> items = itemService.getAllItems();
for (Item item : items) {
logger.info("Item details: {}", item);
}
return ResponseEntity.ok(items);
}

@GetMapping("/{id}")
public ResponseEntity<Item> getItemById(@PathVariable Long id) {
logger.info("GET /items/{} called", id);
if (id == null) {
throw new InvalidParamsException("Item id must be provided");
}
Item item = itemService.getItemById(id);
return ResponseEntity.ok(item);
}

@PostMapping
public ResponseEntity<Item> createItem(@RequestBody Item item) {
logger.info("POST /items called with item: {}", item);
if (item == null || item.getName() == null || item.getName().isBlank() || item.getPrice() == null) {
throw new InvalidParamsException("Item name and price must be provided");
}
Item createdItem = itemService.createItem(item);
return ResponseEntity.ok(createdItem);
}

@PutMapping("/{id}")
public ResponseEntity<Item> updateItem(@PathVariable Long id, @RequestBody Item item) {
logger.info("PUT /items/{} called with item: {}", id, item);
if (id == null || item == null || item.getName() == null || item.getName().isBlank() || item.getPrice() == null) {
throw new InvalidParamsException("Item id, name, and price must be provided");
}
Item updatedItem = itemService.updateItem(id, item);
return ResponseEntity.ok(updatedItem);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteItem(@PathVariable Long id) {
logger.info("DELETE /items/{} called", id);
if (id == null) {
throw new InvalidParamsException("Item id must be provided");
}
itemService.deleteItem(id);
return ResponseEntity.noContent().build();
}
}

Testing Exception Handling​

You can test the exception handling by:

  1. Testing ItemNotFoundException:

    • Send a GET request to /items/999 (assuming no item with ID 999 exists)
    • Send a PUT request to /items/999 with valid data
    • Send a DELETE request to /items/999
  2. Testing InvalidParamsException:

    • Send a POST request to /items with missing name or price
    • Send a POST request to /items with empty JSON {}
    • Send a PUT request with missing data
  3. Expected Responses:

    • ItemNotFoundException: HTTP 404 with error message
    • InvalidParamsException: HTTP 400 with error message
    • Any other exception: HTTP 500 with generic error message

The global exception handler will automatically catch these exceptions and return appropriate HTTP status codes and error messages, providing a consistent API error response format.