What is better: extend Exception or RuntimeException?
In Java there are two main types of exceptions:
Junior Level
Key question: should the caller be forced to handle this error?
Decision Tree:
- Input/validation error ->
IllegalArgumentException(unchecked) - Business rule error ->
BusinessException(unchecked) - External system error -> depends:
- If recoverable (retry helps) -> checked
- If not (service down) -> unchecked
In Java there are two main types of exceptions:
- Checked Exception (inherit from
Exception, but notRuntimeException) - compiler requires handling. Why: so the developer consciously decides at write time how to react to this error. It’s a form of documentation built into the language. - Unchecked Exception (inherit from
RuntimeException) - compiler does not require handling. Why: JVM is designed so that these exceptions represent programming errors (bugs) that cannot be recovered on the fly - they need to be fixed in code, not caught.
// Checked - must declare throws or catch
public class DatabaseException extends Exception {
public DatabaseException(String msg) { super(msg); }
}
// Unchecked - no need to declare
public class ValidationException extends RuntimeException {
public ValidationException(String msg) { super(msg); }
}
Recommendation (not an absolute rule):
- User errors (wrong input, business rule violations) -> usually
RuntimeException, because calling code cannot recover - it must fix the logic - External system errors (network, DB, filesystem) -> often
Exception, because calling code can take action: retry, switch to backup, notify user
Usage example
// Checked - external system unavailable
public User findUser(Long id) throws DatabaseException {
// SQL may fail - calling code MUST know this
// In classic JDBC - checked (SQLException). In Spring Data/JPA - unchecked (DataAccessException). Choice depends on framework.
return userRepository.findById(id)
.orElseThrow(() -> new DatabaseException("User not found: " + id));
}
// Unchecked - programming error
public void validateAge(int age) {
if (age < 0) {
throw new ValidationException("Age cannot be negative");
}
}
Middle Level
When to extend Exception (Checked)
Use checked exception when:
- Caller can recover
public Connection getConnection() throws ConnectionPoolExhaustedException { // Caller can wait and try again } - It’s an expected situation, not a bug
public Document readDocument(String path) throws FileNotFoundException { // File may not exist - this is normal } - API requires mandatory handling
public void transferMoney(Account from, Account to, BigDecimal amount) throws InsufficientFundsException { // Caller MUST handle insufficient funds }
When to extend RuntimeException (Unchecked)
Use unchecked exception when:
- Programming error (bug, wrong arguments)
public User getUser(Long id) { if (id == null) { throw new IllegalArgumentException("ID cannot be null"); } // ... } - Recovery is impossible
public Config loadConfig(String path) { if (!Files.exists(path)) { throw new ConfigurationException("Config file missing: " + path); // Program cannot work without configuration } } - Spring / modern style - most frameworks prefer unchecked
@Service public class OrderService { public Order createOrder(OrderRequest req) { if (req.items().isEmpty()) { throw new BusinessRuleException("Order must have items"); } // Spring handles RuntimeException -> 400 Bad Request } }
Comparison table
| Criterion | Exception (Checked) | RuntimeException (Unchecked) |
|---|---|---|
| Mandatory handling | Yes | No |
| Declaration in throws | Required | Not required |
| Typical use | External systems, I/O | Logic errors, validation |
| Recovery | Possible | Usually impossible |
| Spring handling | Needs @ControllerAdvice |
Automatic -> 400/500 |
| Code verbosity | High (try/catch everywhere) | Low |
Typical mistakes
- Checked exception for validation: ```java // Bad - validation doesn’t need recovery, calling code can’t fix invalid email on the fly // Also, checked exception forces try/catch everywhere, making code harder to read public void setEmail(String email) throws InvalidEmailException { … }
// Good - RuntimeException, because it’s a programming error (passed wrong email) public void setEmail(String email) { if (!isValid(email)) { throw new InvalidEmailException(email); // RuntimeException } }
2. **Wrapping checked in unchecked without reason:**
```java
// Loses information about the real problem
try {
fileInputStream.read();
} catch (IOException e) {
throw new RuntimeException(e); // Lose context!
}
// Wrap preserving cause and context
try {
fileInputStream.read();
} catch (IOException e) {
throw new DataReadException("Failed to read file: " + filePath, e);
}
When NOT to use RuntimeException
Don’t use RuntimeException (i.e., choose checked Exception) if:
- You write a public library - checked exception serves as documentation: developer immediately sees what errors are possible and must handle them
- Calling code can recover - e.g., temporary service unavailability (retry helps), or file is locked by another process (wait and retry)
- Critical system: ignoring error = financial loss or security threat. Checked exception - insurance against silent failure
- Project without a single global error handler - unchecked exception will pass unnoticed, checked will at least force thinking about handling
When NOT to use checked Exception
Don’t use checked Exception (i.e., choose RuntimeException) if:
- It’s a programming error -
nullinstead of object, negative age, empty required list. Such errors need to be fixed in code, not caught - You use Spring/Web framework - framework already has global handler (
@ControllerAdvice) that turns RuntimeException into correct HTTP status - Too much boilerplate - if
throwsgoes through 10 layers of application, it’s noise, not useful information
Senior Level
Architectural perspective
Checked exceptions were a Java experiment that didn’t catch on in other languages:
- C# - only unchecked
- Kotlin - only unchecked
- Go - multiple return values (error, result)
- Rust -
Result<T, E>(explicit handling at type level)
Problem with checked exceptions: abstraction leak
// DAO layer "leaks" into Controller through SQLException
public interface UserRepository {
User findById(Long id) throws SQLException; // SQLException - implementation detail!
}
// Wrap in domain exception
public interface UserRepository {
User findById(Long id); // Clean interface
// Inside: catch (SQLException e) -> throw new RepositoryException(e)
}
Spring and checked exceptions
Spring converts most checked exceptions to unchecked:
@Repository
public class JpaUserRepository {
// JPA throws PersistenceException (unchecked)
// Spring converts to DataAccessException (unchecked)
// ControllerAdvice catches -> HTTP status
}
Performance
At runtime there is no difference - both create stack trace. The difference is in development: checked require throws declaration in all methods up the stack, unchecked - don’t. This is development overhead, not execution. Creating an exception (stack trace) is expensive (~1-5 microseconds), but inheritance type doesn’t affect this.
Modern Java Approach
// Domain Exceptions - unchecked, with context
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
private final Long orderId;
public Long getOrderId() { return orderId; }
}
// Infrastructure Exceptions - unchecked, with cause
public class ExternalServiceException extends RuntimeException {
public ExternalServiceException(String serviceName, Throwable cause) {
super("External service failed: " + serviceName, cause);
this.serviceName = serviceName;
}
private final String serviceName;
}
// Retryable Exceptions - unchecked, with marker
public class TransientException extends RuntimeException {
// Marks exceptions that can be retried
}
Decision Framework
Questions for decision making:
|
+-- Can caller RECOVER?
| +-- Yes -> Exception (checked)
| +-- No -> RuntimeException (unchecked)
|
+-- Is this a PROGRAMMING error?
| +-- Yes -> RuntimeException (IllegalArgumentException, IllegalStateException)
| +-- No -> Look further
|
+-- Is this an API for third-party developers?
| +-- Yes -> Exception (forced handling)
| +-- No -> RuntimeException (less boilerplate)
|
+-- Does framework handle exceptions? (Spring)
+-- Yes -> RuntimeException (framework handles)
+-- No -> Depends on context
Best Practices
// Domain exception hierarchy
BaseException (RuntimeException)
+-- ValidationException
+-- NotFoundException
+-- BusinessRuleException
+-- ExternalServiceException
// Infrastructure exception hierarchy
InfrastructureException (RuntimeException)
+-- DatabaseException
+-- NetworkException
+-- TimeoutException
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
return ResponseEntity.status(404).body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(ValidationException.class)
ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
return ResponseEntity.status(400).body(new ErrorResponse(e.getMessage()));
}
}
Interview Cheat Sheet
Must know:
Exception(checked) - compiler requires handling;RuntimeException(unchecked) - doesn’t- Choose checked when caller can recover (retry, fallback); unchecked - for programming errors
- In Spring/Web frameworks prefer unchecked -
@ControllerAdviceautomatically maps to HTTP statuses - Checked exceptions - Java experiment, didn’t catch on in C#, Kotlin, Go, Rust
- Wrap checked in unchecked preserving cause (
new DataReadException("msg", e)) - Decision Framework: can recover -> checked; error is bug -> unchecked; public library -> checked
Frequent follow-up questions:
- Why does Spring prefer unchecked? - Framework already has global handler; checked add boilerplate without benefit
- What is abstraction leak through checked exceptions? - When
SQLException“leaks” from DAO to Controller throughthrows- implementation detail gets into interface - When is checked exception better? - Public libraries, critical systems (finance, security), temporary failures (retry helps)
- Is there a performance difference? - At runtime no, both create stack trace; difference is in development overhead (throws through all layers)
Red flags (NOT to say):
- “I always use checked - it’s safer” - This leads to boilerplate and abstraction leaks
- “I wrap all checked in RuntimeException without reason” - Information about real problem is lost
- “Checked exception = slower” - No runtime difference, difference is only in code amount
Related topics:
- [[13. Can you create custom exceptions]]
- [[14. When should you create your own exceptions]]
- [[2. What is a checked exception and when to use it]]
- [[3. What is an unchecked exception (Runtime Exception)]]
- [[18. What is exception wrapping (wrapping)]]