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
@ControllerAdviceThis 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:
-
Testing ItemNotFoundException:
- Send a GET request to
/items/999(assuming no item with ID 999 exists) - Send a PUT request to
/items/999with valid data - Send a DELETE request to
/items/999
- Send a GET request to
-
Testing InvalidParamsException:
- Send a POST request to
/itemswith missing name or price - Send a POST request to
/itemswith empty JSON{} - Send a PUT request with missing data
- Send a POST request to
-
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.