← Back to list

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…

Tharindu Dulshan · 2026-04-04 10:09 · 10 claps · 3.6 min read
#spring-boot #java #backend-development #retry #exception-handling
Open on Medium ↗
Wiki topics: 🌐 · Web Development

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,

  1. Validate First — Let your request objects enforce rules using annotations instead of relying on exceptions for basic validation.
  2. Hierarchy of Custom Exceptions — Define meaningful exceptions that represent real business problems.
  3. 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.

  1. 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());
    }
}
  1. 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


메타데이터
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