Exception Handling in Java: From Throwable to Global Exception Handling in Spring Boot

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

FeatureCheckedRuntime
Compiler checksYesNo
Must handleYesNo
ExtendsExceptionRuntimeException
RecoverableUsuallyUsually 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.

Leave a Reply

Your email address will not be published. Required fields are marked *