Question 16 · Section 7

What is better: extend Exception or RuntimeException?

In Java there are two main types of exceptions:

Language versions: English Russian Ukrainian

Junior Level

Key question: should the caller be forced to handle this error?

Decision Tree:

  1. Input/validation error -> IllegalArgumentException (unchecked)
  2. Business rule error -> BusinessException (unchecked)
  3. 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 not RuntimeException) - 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:

  1. Caller can recover
    public Connection getConnection() throws ConnectionPoolExhaustedException {
     // Caller can wait and try again
    }
    
  2. It’s an expected situation, not a bug
    public Document readDocument(String path) throws FileNotFoundException {
     // File may not exist - this is normal
    }
    
  3. 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:

  1. Programming error (bug, wrong arguments)
    public User getUser(Long id) {
     if (id == null) {
         throw new IllegalArgumentException("ID cannot be null");
     }
     // ...
    }
    
  2. Recovery is impossible
    public Config loadConfig(String path) {
     if (!Files.exists(path)) {
         throw new ConfigurationException("Config file missing: " + path);
         // Program cannot work without configuration
     }
    }
    
  3. 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

  1. 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 - null instead 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 throws goes 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 - @ControllerAdvice automatically 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 through throws - 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)]]