Think Before You Catch: 3 Rules Every Developer Should Know
This article targets the beginner level java developers(Spring-Boot). If you’ve been writing Java code for a while, chances are you’ve…
Think Before You Catch: 3 Rules Every Developer Should Know

This article targets the beginner level java developers(Spring-Boot). If you’ve been writing Java code for a while, chances are you’ve fallen into this habit of wrapping everything up with try-catch blocks. As you keep going on you might have over used try-catch blocks. Excessive try-catch usage is often a sign of poor design rather than good error handling.
I’m not saying that try-catch blocks should be totally replaced but you should know when to use it and when not to.
In this article I will be covering 3 points that may help you,
- Validate First — Let your request objects enforce rules using annotations instead of relying on exceptions for basic validation.
- Hierarchy of Custom Exceptions — Define meaningful exceptions that represent real business problems.
- Global Exception Handling with
@ControllerAdvice–Centralize error handling instead of repeating it in every method.
At the end, I hope this may help you all to write better, redable and structured code.
1. Validate First
sometimes developers uses try-catch blocks to capture exceptions thrown because of invalid/bad inputs, Null inputs. For example you might have used a try catch to throw an exception when user enters an invalid email address.
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestBody UserRequest request) {
try {
if (!request.getEmail().contains("@")) {
throw new RuntimeException("Invalid email format");
}
if (request.getName() == null) {
throw new NullPointerException("Name cannot be null");
}
// imagine saving the user here
return ResponseEntity.ok("User registered successfully");
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
instead of doing the above we can simply validate in the request object.
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class UserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
// getters & setters
}
2. Hierarchy of Custom Exceptions
Another common mistake is catching generic exceptions like Exception and trying to figure out what went wrong after the failure.
// generic exceptions will not tell you exactly what's wrong
try {
userService.process(user);
} catch (Exception e) {
logger.error("Something failed: " + e.getMessage());
return ResponseEntity.internalServerError().build();
}
To fix this we can use pre written custom exceptions in java. Instead of catching everything, you can define exceptions that clearly describe the failure.
Base Exception
public class AppException extends RuntimeException {
private final HttpStatus status;
private final String errorCode;
public AppException(String message, HttpStatus status, String errorCode) {
super(message);
this.status = status;
this.errorCode = errorCode;
}
public HttpStatus getStatus() {
return status;
}
public String getErrorCode() {
return errorCode;
}
}
Specific Exceptions
public class ProductNotFoundException extends AppException {
public ProductNotFoundException(Long productId) {
super("Product not found with id: " + productId,
HttpStatus.NOT_FOUND,
"PRODUCT_NOT_FOUND");
}
}
public class ProductOutOfStockException extends AppException {
public ProductOutOfStockException(String productName) {
super("Product out of stock: " + productName,
HttpStatus.CONFLICT,
"PRODUCT_OUT_OF_STOCK");
}
}
Usage in Service Layer
public Product purchaseProduct(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
if (product.getStock() <= 0) {
throw new ProductOutOfStockException(product.getName());
}
product.setStock(product.getStock() - 1);
return productRepository.save(product);
}
3. Global Exception Handling with @ControllerAdvice
As of now you have meaning full exceptions written the next thing to do is write a global exception handler to handle all exceptionjs at one place.
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AppException.class)
public ResponseEntity<ErrorResponse> handleAppException(AppException ex) {
ErrorResponse error = new ErrorResponse(
ex.getErrorCode(),
ex.getMessage()
);
return new ResponseEntity<>(error, ex.getStatus());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"Something went wrong"
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Error Response Model
public class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// getters & setters
}
Bonus: Retrying Operations Safely
Sometimes failures are transient. For example, calling a payment gateway or an external API may fail temporarily. Instead of wrapping everything in try-catch and manually retrying, Spring provides a clean way to handle retries using Spring Retry.
- Add Spring Retry Dependency
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Enable retry in your application:
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableRetry
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2. Retry on Specific Exceptions
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
@Retryable(
value = { NetworkException.class }, // Retry only on this exception
maxAttempts = 3, // Try 3 times
backoff = @Backoff(delay = 2000) // Wait 2 seconds between retries
)
public void processPayment(Order order) {
// Fake API Call
if (Math.random() < 0.7) {
throw new NetworkException("Temporary network failure");
}
System.out.println("Payment processed successfully for order: " + order.getId());
}
}
- Optional: Handle Retry Exhaustion
You can catch the failure after all retries are exhausted using @Recover:
import org.springframework.retry.annotation.Recover;
@Recover
public void recover(NetworkException e, Order order) {
// Log and take alternate action
System.out.println("Failed after retries: " + e.getMessage() + " for order " + order.getId());
}
Note : Don’t retry all exceptions blindly. Only transient, recoverable errors should be retried, permanent failures like validation errors should fail fast.
Summary
Try-catch is not bad, over using and also misusing it is bad.
References
- Spring Boot Official Documentation — **Validation**
- **Jakarta Bean Validation **(JSR 380)
- Spring Boot Exception Handling with **@ControllerAdvice**
- Building a **Custom Exception Hierarchy **in Java
- **Spring Retry **— Declarative Retry for Spring Applications
메타데이터
- post_id
- 522c9bb5178f
- slug
- think-before-you-catch-3-rules-every-developer-should-know-522c9bb5178f
- url
- https://medium.com/@tharindudulshanfdo/think-before-you-catch-3-rules-every-developer-should-know-522c9bb5178f
- canonical_url
- https://medium.com/@tharindudulshanfdo/think-before-you-catch-3-rules-every-developer-should-know-522c9bb5178f
- author_url
- https://medium.com/@tharindudulshanfdo
- status
- ok
- fetched_at
- 2026-07-11 12:56:19