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
Strategy | Pros | Cons |
---|---|---|
One Exception per Input | Max granularity | Too many classes, high maintenance |
Generic Exception Only | Simple | Hard to trace errors, poor UX/API feedback |
Domain-Based Custom Exceptions (✅) | Balanced, readable, scalable | Slightly 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.