✅ Recommended Strategy

exception

Use a Hierarchy of Custom Exceptions with Specific Messages

1. Define a base exception class

Create a generic custom exception that extends RuntimeException (or Exception if you need checked exceptions).

public class ApplicationException extends RuntimeException {
    private final String errorCode;

    public ApplicationException(String message, String errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

2. Create a few meaningful subclasses for common domains

Use subclasses only for distinct categories, not every input field.

public class ValidationException extends ApplicationException {
    public ValidationException(String message) {
        super(message, "VALIDATION_ERROR");
    }
}

public class ResourceNotFoundException extends ApplicationException {
    public ResourceNotFoundException(String message) {
        super(message, "RESOURCE_NOT_FOUND");
    }
}

3. Throw exceptions with detailed messages

Use informative messages, but reuse exception classes.

if (email == null || !email.contains("@")) {
    throw new ValidationException("Invalid email address: " + email);
}

if (user == null) {
    throw new ResourceNotFoundException("User with ID " + userId + " not found.");
}

4. Centralize exception handling

Use @ControllerAdvice in Spring Boot to map exceptions to HTTP responses.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ApiError> handleValidationException(ValidationException ex) {
        return ResponseEntity.badRequest().body(new ApiError(ex.getErrorCode(), ex.getMessage()));
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleNotFoundException(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                             .body(new ApiError(ex.getErrorCode(), ex.getMessage()));
    }
}

🔄 Trade-offs

StrategyProsCons
One Exception per InputMax granularityToo many classes, high maintenance
Generic Exception OnlySimpleHard to trace errors, poor UX/API feedback
Domain-Based Custom Exceptions (✅)Balanced, readable, scalableSlightly more boilerplate

📌 Summary

  • Don’t create one exception class per field or input.
  • Don’t rely solely on generic exceptions with vague messages.
  • Do create a few well-named custom exceptions per domain concern (validation, business rule, resource not found).
  • Do include specific messages and codes for clarity and front-end/API consumption.

If you’re building APIs, error codes + structured JSON responses improve DX (developer experience) and UX.

Let me know if you want a boilerplate for API exception handling in Spring Boot.


Other considerations

There are several important considerations when designing and handling exceptions in Java applications, especially for APIs or enterprise-level systems. Here’s a breakdown of strategic, practical, and technical points beyond the basic structure:


🔍 1. Checked vs Unchecked Exceptions

  • Checked Exceptions (extends Exception): Force callers to handle or declare them.
    • Good for recoverable errors (e.g. file not found, invalid config).
    • Can pollute method signatures and reduce readability.
  • Unchecked Exceptions (extends RuntimeException): Don’t require handling.
    • Better for programming errors, validation failures, or business rules.
    • Preferred in modern Java and frameworks like Spring.

👉 Recommendation: Use unchecked exceptions for business logic and REST APIs unless you’re dealing with truly recoverable scenarios (e.g., IO or external service failures).


🧩 2. Message Structure: Human + Machine Friendly

  • Provide:
    • A clear human-readable message
    • An error code (short, unique identifier, e.g. USER_NOT_FOUND)
    • Optionally: a field name, if applicable
{
  "error": {
    "code": "EMAIL_INVALID",
    "message": "The email format is invalid",
    "field": "email"
  }
}

This format helps:

  • Frontend display and localization
  • Developers debugging issues
  • Automated systems parsing errors

🗂 3. Use of ErrorCode Enum or Registry

Instead of string literals scattered throughout your code, centralize error codes:

public enum ErrorCode {
    INVALID_EMAIL("EMAIL_INVALID"),
    USER_NOT_FOUND("USER_NOT_FOUND");

    private final String code;

    ErrorCode(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

This:

  • Prevents duplication
  • Enables translation
  • Supports analytics/logs

📦 4. Payload-rich Exceptions

Sometimes, it’s useful for exceptions to carry more context, like field names, rejected values, etc.

public class FieldValidationException extends ValidationException {
    private final String field;
    private final Object rejectedValue;

    public FieldValidationException(String field, Object rejectedValue, String message) {
        super(message);
        this.field = field;
        this.rejectedValue = rejectedValue;
    }

    // getters
}

You can serialize this in your @ControllerAdvice.


🧪 5. Testability and Observability

  • Design exceptions so that unit and integration tests can assert them cleanly.
  • Log exceptions with structured messages or MDC context (e.g. requestId).
  • Use tools like:
    • Sentry, Datadog, or Elastic Stack for centralized logging
    • AOP or HandlerInterceptor to inject context (user ID, trace ID)

💡 6. Avoid Swallowing Exceptions

Don’t silently ignore exceptions unless you’re 100% sure it’s safe:

try {
    doSomething();
} catch (Exception e) {
    // ❌ Don't just do nothing or log without action
    logger.error("Something went wrong", e); // ✅
    throw new ApplicationException("Internal processing error", "INTERNAL_ERROR");
}

🔒 7. Security and Internal Data

Be cautious not to expose internal details like stack traces, SQL errors, or implementation-specific info in API responses.

✅ Do this:

{
  "code": "INTERNAL_ERROR",
  "message": "Something went wrong. Please contact support."
}

❌ Not this:

{
  "message": "java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'foo@example.com'..."
}

🧘 Final Thoughts

Designing a good exception strategy is about clarity, maintainability, and robustness. A few key principles:

  • Be consistent in your error structure.
  • Surface enough context for clients to recover or correct input.
  • Don’t overdo exception classes, but don’t go fully generic either.
  • Integrate with observability tools to monitor exception patterns.

Comments

Leave a Reply