Understanding Exceptions in Java

Exceptions in Java
Handling exceptional conditions that may occur during system execution is a critical aspect of application development. While compilers can catch syntax errors effectively, they offer limited support for runtime issues. Therefore, developers must anticipate and account for various exceptional situations in their code.
1. Error vs. Exception
Java distinguishes two major categories of problems that may arise during runtime: errors and exceptions.
- Errors represent serious issues at the system level, such as JVM failures or memory exhaustion. These are typically outside the control of application developers and should not be handled using
try-catch
blocks. When an error occurs, the application should either terminate or take predefined recovery steps. - Exceptions, in contrast, indicate recoverable problems that occur during program execution. These include missing files or network disconnections—issues that developers are expected to handle gracefully.
All errors and exceptions in Java inherit from the base class Throwable
, which is divided into two main subclasses: Error
and Exception
. This inheritance hierarchy enables clear categorization and facilitates targeted handling strategies.
Object
└── Throwable
├── Error
│ └── ...
└── Exception
└── ...
2. Checked vs. Unchecked Exceptions
Exceptions in Java are further categorized into checked and unchecked exceptions, based on how they are enforced by the compiler.
Exception
├── RuntimeException
│ ├── IndexOutOfBoundsException
│ │ └── ArrayIndexOutOfBoundsException
│ ├── NullPointerException
│ └── ...
├────── IOException
│ ├── FileNotFoundException
│ └── ...
├────── SQLException
│ ├── SQLSyntaxErrorException
│ └── ...
└────── ...
2-1. Checked Exceptions
Checked exceptions are enforced by the compiler. They are typically associated with operations that interact with external systems—such as file I/O, database connections, or network access.
Developers must either handle these exceptions using try-catch
blocks or explicitly declare them using the throws
keyword. Failure to do so results in a compile-time error.
public void readData() throws IOException {
FileReader reader = new FileReader("data.txt");
}
While this enforcement promotes robustness, it can also lead to exception propagation across multiple layers, introducing tight coupling and reducing maintainability—especially when higher-level components cannot meaningfully handle the exception.
To illustrate:
class Controller {
public void handleRequest() throws SQLException {
service.process();
}
}
class Service {
public void process() throws SQLException {
repository.query();
}
}
class Repository {
public void query() throws SQLException {
throw new SQLException("Query failed");
}
}
Here, a SQLException
thrown from the Repository
must be declared or caught at each level, even if upper layers cannot respond to it meaningfully.
Because of this, many modern Java applications prefer unchecked exceptions for internal logic.
2-2. Unchecked Exceptions
Unchecked exceptions are not enforced at compile time. They represent programming errors such as null dereferences or illegal array access. These exceptions inherit from RuntimeException
.
Since the compiler does not require handling or declaration, they allow for cleaner code and decoupled exception management. However, this flexibility can lead to missed exception handling if not properly documented and tested.
class Service {
public void process() {
repository.query();
}
}
class Repository {
public void query() {
throw new RuntimeException("Unexpected error");
}
}
Unchecked exceptions are well-suited for application-level logic, where exceptions are caught and handled centrally—for instance, in a global exception handler. While less verbose, developers should still document the exception behavior clearly, and optionally declare them with throws
for better tooling support.
public class Repository {
public void save() throws CustomRuntimeException { ... }
}
Although Java's early design philosophy emphasized checked exceptions, the community has largely shifted toward unchecked exceptions for maintainability and simplicity. Frameworks like JPA and Spring follow this pattern:
public class PersistenceException extends RuntimeException {
...
}
It is also worth noting that Java's Error
class is technically an unchecked type but represents non-recoverable conditions and should never be caught explicitly.
3. Rollback Strategies by Exception Types in Spring Transactions
Spring Framework handles exceptions differently depending on the exception type, particularly in transactional methods annotated with @Transactional
.
- Unchecked exceptions trigger a rollback.
- Checked exceptions do not trigger a rollback by default.
Developers can override this behavior:
public class Service {
@Transactional(rollbackFor = Exception.class)
public void process() throws Exception {
...
}
}
Or prevent rollback for specific exceptions:
public class Service {
@Transactional(noRollbackFor = BusinessException.class)
public void process() {
...
}
}
This flexible configuration allows precise control over transaction boundaries in business logic.
- 📺 Inflearn