Introduction
Exception handling is one of the most fundamental concepts in Java. Every Java developer uses exceptions daily, yet many developers struggle to explain:
- Why exceptions exist
- Difference between Exception and RuntimeException
- Difference between checked and unchecked exceptions
- How exception inheritance works
- How exceptions propagate across layers
- Best practices in Spring Boot applications
- How to design enterprise-grade exception handling frameworks
In this article, we will explore Java exception handling from the JVM level all the way to Spring Boot REST APIs.
Why Do We Need Exceptions?
Consider the following code:
public User getUser(Long id) {
return userRepository.findById(id).get();
}
What happens if:
- Database is unavailable?
- User does not exist?
- Connection timeout occurs?
Without exceptions, every method would need to return error codes:
int result = saveUser();
if(result == -1){
// database error
}
This quickly becomes difficult to maintain.
Exceptions provide a clean mechanism to separate:
- Business logic
- Error handling logic
Java Exception Hierarchy
The complete exception hierarchy starts from:
Object
|
+-- Throwable
|
+-- Error
|
+-- Exception
|
+-- RuntimeException
Visual representation:
Throwable
│
├── Error
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
│
└── Exception
│
├── IOException
├── SQLException
├── ParseException
│
└── RuntimeException
├── NullPointerException
├── IllegalArgumentException
├── IndexOutOfBoundsException
├── ArithmeticException
└── NumberFormatException
Throwable
Every exception in Java derives from Throwable.
public class Throwable
Throwable contains:
String message
Throwable cause
StackTraceElement[]
Example:
throw new Throwable("Something went wrong");
Generally, developers never throw Throwable directly.
Error
Errors indicate serious JVM-level failures.
Examples:
OutOfMemoryError
StackOverflowError
NoClassDefFoundError
VirtualMachineError
Example:
List<byte[]> data = new ArrayList<>();
while(true){
data.add(new byte[1024 * 1024]);
}
Eventually:
OutOfMemoryError
Should Errors Be Caught?
Generally:
catch(Error e)
is considered a bad practice.
Errors indicate situations from which the application usually cannot recover.
Exception
Exception represents recoverable application problems.
Examples:
File not found
Database unavailable
Validation failed
User not found
Checked Exceptions
Checked exceptions are verified at compile time.
Examples:
IOException
SQLException
ClassNotFoundException
ParseException
Example:
public void readFile() throws IOException {
}
Compiler forces handling:
try {
readFile();
}
catch(IOException ex){
}
or
public void process() throws IOException {
readFile();
}
Why Checked Exceptions Exist?
Java designers wanted developers to explicitly acknowledge recoverable situations.
Example:
File file = new File("test.txt");
File may not exist.
Compiler forces consideration of failure.
Runtime Exceptions
Runtime exceptions occur during program execution.
Examples:
NullPointerException
IllegalArgumentException
ArithmeticException
ClassCastException
Example:
String name = null;
name.length();
Result:
NullPointerException
No compiler warning.
Checked vs Runtime Exceptions
| Feature | Checked | Runtime |
|---|---|---|
| Compiler checks | Yes | No |
| Must handle | Yes | No |
| Extends | Exception | RuntimeException |
| Recoverable | Usually | Usually Programming Bug |
Examples:
Checked:
IOException
SQLException
Runtime:
NullPointerException
IllegalStateException
Exception Inheritance
Consider:
class BusinessException extends Exception
class ValidationException extends BusinessException
throw new ValidationException();
Can be caught as:
catch(ValidationException e)
or
catch(BusinessException e)
or
catch(Exception e)
or
catch(Throwable e)
because of inheritance.
Catch Block Ordering
Incorrect:
try {
}
catch(Exception ex){
}
catch(IOException ex){
}
Compiler error.
Reason:
Exception
already handles:
IOException
Correct:
try {
}
catch(IOException ex){
}
catch(Exception ex){
}
Always catch most specific exception first.
Exception Propagation
Suppose:
method1()
|
method2()
|
method3()
public void method3() {
throw new RuntimeException();
}
If method3 doesn’t handle exception:
method3
→
method2
→
method1
→
JVM
Exception propagates upward until handled.
Exception Chaining
Bad:
catch(SQLException ex){
throw new RuntimeException();
}
Original exception lost.
Good:
catch(SQLException ex){
throw new RuntimeException(
"Database failure",
ex
);
}
Output:
RuntimeException
caused by
SQLException
Custom Exceptions
Enterprise applications typically create custom exceptions.
Example:
public class UserNotFoundException
extends RuntimeException {
public UserNotFoundException(String msg){
super(msg);
}
}
Usage:
throw new UserNotFoundException(
"User not found"
);
Exception Handling in Spring Boot
Typical architecture:
Controller
|
Service
|
Repository
|
Database
Repository Layer
@Repository
public interface UserRepository
extends JpaRepository<User,Long> {
}
JPA exceptions:
DataIntegrityViolationException
JpaSystemException
ConstraintViolationException
originate here.
Service Layer
@Service
public class UserService {
public User getUser(Long id){
return repository.findById(id)
.orElseThrow(
() -> new UserNotFoundException(
"User not found"
));
}
}
Service layer converts technical exceptions into business exceptions.
Controller Layer
@GetMapping("/{id}")
public User getUser(
@PathVariable Long id){
return userService.getUser(id);
}
Controller should not contain try-catch blocks everywhere.
Bad:
try{
}
catch(Exception e){
}
in every endpoint.
Global Exception Handling
Spring provides:
@RestControllerAdvice
Example:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(
UserNotFoundException.class)
public ResponseEntity<ApiError>
handleUserNotFound(
UserNotFoundException ex){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(
new ApiError(
ex.getMessage()
));
}
}
Now all controllers automatically use this handler.
Exception Flow in Spring
Repository
|
| throws
V
DataIntegrityViolationException
|
Service
|
| wraps
V
CustomerCreationException
|
Controller
|
V
@RestControllerAdvice
|
V
HTTP 400 Response
This is the preferred enterprise pattern.
Best Practice: Exception Translation
Bad:
throw new SQLException();
from service layer.
Good:
catch(SQLException ex){
throw new CustomerCreationException(
"Unable to create customer",
ex
);
}
Service layer should expose business exceptions, not database exceptions.
Common Enterprise Exception Hierarchy
ApplicationException
│
├── ValidationException
├── BusinessException
├── ResourceNotFoundException
├── UnauthorizedException
└── IntegrationException
Example:
public abstract class ApplicationException
extends RuntimeException {
}
All business exceptions derive from this.
Checked or Runtime for Custom Exceptions?
Most modern Spring applications prefer:
extends RuntimeException
instead of:
extends Exception
Reasons:
- Cleaner code
- Less boilerplate
- Easier propagation
- Works naturally with Spring transactions
Example:
public class ProductNotFoundException
extends RuntimeException {
}
Exception Handling with CompletableFuture
Exceptions are wrapped inside:
CompletionException
Example:
CompletableFuture.supplyAsync(
() -> {
throw new RuntimeException();
});
Handle:
future.exceptionally(ex -> {
log.error("Error", ex);
return null;
});
or
future.handle(...)
Exception Handling Best Practices
DO
✅ Create custom business exceptions
✅ Use @RestControllerAdvice
✅ Preserve original exception
throw new MyException(msg, ex);
✅ Log once
✅ Return meaningful error responses
DON’T
❌ Catch Exception everywhere
❌ Swallow exceptions
catch(Exception e){
}
❌ Expose SQL exceptions to clients
❌ Catch Throwable
❌ Catch Error
❌ Log and rethrow repeatedly
Interview Questions
What is the root class of all exceptions?
Answer:
Throwable
Difference between Error and Exception?
Error indicates JVM failures.
Exception indicates recoverable application failures.
Difference between checked and runtime exception?
Checked exceptions are compiler enforced.
Runtime exceptions are not.
Why is NullPointerException unchecked?
Because it indicates a programming bug rather than a recoverable business condition.
What happens if exception is not caught?
It propagates up the call stack until JVM handles it.
Why use @RestControllerAdvice?
To centralize exception handling and avoid duplicate try-catch blocks in controllers.
Conclusion
Exception handling is much more than writing try-catch blocks. Understanding the Throwable hierarchy, checked versus unchecked exceptions, propagation, exception translation, and Spring Boot global exception handling is essential for building enterprise-grade Java applications.
A well-designed exception strategy improves:
- Maintainability
- Observability
- API consistency
- Debugging
- Production support
The recommended modern Spring Boot approach is:
Repository Exceptions
↓
Service Business Exceptions
↓
@RestControllerAdvice
↓
Standard API Error Response
This provides clean separation of concerns and a scalable exception handling framework suitable for microservices and enterprise systems.