Treat Throwable as the root of Java failure handling, but do not treat all throwables the same. Most application code should catch specific Exception types, rarely catch Error, and almost never catch Throwable directly.
TLDR: Exception usually represents conditions your program may recover from, such as a missing file or invalid input. Error usually signals serious JVM or environment failures, such as OutOfMemoryError, where recovery is unreliable. For example, in a payment service processing 100,000 requests per day, catching IOException and retrying once may recover 1–3% of failed network calls, while catching Throwable could hide fatal JVM problems and make outages harder to diagnose. Use narrow catches, preserve the cause, and let severe errors fail loudly.
The Throwable hierarchy in plain terms
In Java, every object that can be thrown with throw must extend java.lang.Throwable. This class sits at the top of the failure hierarchy. Under it are two major branches: Exception and Error.
That split matters. It tells you whether the condition is something normal application logic may handle, or something so serious that the runtime itself may be compromised.
Throwable: the root type for anything that can be thrown.Exception: problems an application may reasonably handle.RuntimeException: unchecked exceptions caused by programming mistakes or invalid runtime conditions.Error: serious failures usually outside application control.
Exception vs Error
Exception is intended for recoverable or expected failure paths. A file may not exist. A database connection may time out. A user may submit invalid data. These are not happy paths, but they are part of real software.
Error is different. It usually means the JVM, class loading system, memory model, or runtime environment has hit a serious issue. Examples include OutOfMemoryError, StackOverflowError, NoClassDefFoundError, and LinkageError.
The catch is that both branches are throwables, so Java lets you catch them if you want. That does not mean you should. Catching Error in business logic often gives a false sense of safety. The process may already be unstable.
Checked exceptions
A checked exception is an exception the compiler forces you to handle or declare. It extends Exception but not RuntimeException.
Common examples include:
IOExceptionSQLExceptionClassNotFoundExceptionInterruptedException
Checked exceptions work best when callers can make a real decision. For example, if reading a configuration file fails, the caller might load defaults, ask for a new path, or stop startup with a clear message.
They work poorly when used as noise. A method that declares five checked exceptions can force callers into ugly catch blocks that add no value. Expect to waste time on stack traces if those blocks wrap everything into vague messages such as “operation failed”.
Unchecked exceptions
Unchecked exceptions extend RuntimeException. The compiler does not force callers to catch them. They often signal bugs, invalid method use, or violated assumptions.
Common examples include:
NullPointerExceptionIllegalArgumentExceptionIllegalStateExceptionIndexOutOfBoundsExceptionArithmeticException
Use unchecked exceptions when the caller usually cannot recover at that point. For example, passing a negative page size to a paging method is a programmer error. Throwing IllegalArgumentException is clearer than returning an empty list and hiding the bug.
public List<Order> findOrders(int pageSize) {
if (pageSize <= 0) {
throw new IllegalArgumentException("pageSize must be positive");
}
return repository.findOrders(pageSize);
}
Errors developers should recognize
You should know the major Error types, even if you rarely catch them. They explain many production failures.
OutOfMemoryError: the JVM cannot allocate more memory. Logging may even fail if memory is exhausted.StackOverflowError: often caused by uncontrolled recursion.NoClassDefFoundError: a class was present at compile time but missing at runtime.ExceptionInInitializerError: a static initializer failed.AssertionError: an assertion failed, often in tests or diagnostic code.
Some systems catch Error at a very high boundary to log final diagnostics before shutdown. That can be reasonable in a container, scheduler, or application framework. It is not a good pattern inside ordinary service methods.
Why catching Throwable is dangerous
Catching Throwable catches everything: checked exceptions, unchecked exceptions, errors, and custom throwable types. That is rarely what you want.
It drives me crazy when production code does this:
try {
processOrder(order);
} catch (Throwable t) {
log.error("Order failed");
}
This block hides the real failure. It also discards the stack trace because the throwable is not passed to the logger. Worse, it may swallow OutOfMemoryError or ThreadDeath. The service might keep running in a broken state, returning bad responses for minutes before anyone sees the real cause.
A safer version catches what the method can actually handle:
try {
processOrder(order);
} catch (PaymentTimeoutException e) {
retryPayment(order, e);
} catch (ValidationException e) {
rejectOrder(order, e.getMessage());
}
At system boundaries, a broad catch may be acceptable, but handle it with care:
try {
runJob();
} catch (Exception e) {
log.error("Job failed", e);
markJobFailed();
}
Even there, prefer Exception over Throwable unless you are writing infrastructure code with a clear shutdown policy.
Other Throwable features worth knowing
Causes preserve the original failure. If you wrap an exception, pass the original exception into the constructor. Without this, the stack trace loses its most useful clue.
throw new OrderProcessingException("Could not charge card", e);
Suppressed exceptions appear when one exception happens while another is already being handled. This is common with try-with-resources. For example, a file read may fail, and closing the file may fail too. Java keeps the close failure as a suppressed exception so it is not lost.
InterruptedException deserves special care. Do not swallow it. If your method cannot throw it, restore the interrupt flag.
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceStoppedException("Thread interrupted", e);
}
Practical rules for Java teams
- Catch the narrowest useful type. Prefer
FileNotFoundExceptionorTimeoutExceptionover plainException. - Do not swallow failures. Log the exception object, not just a message.
- Do not catch
Errorin business code. Let the runtime or framework deal with severe failures. - Use checked exceptions for recoverable conditions. Use unchecked exceptions for programming errors and invalid state.
- Create custom exceptions sparingly. A custom type should help callers make a better decision.
- Preserve the cause. Wrapping without the original exception makes debugging harder.
- Fail clearly. A clean crash with a useful stack trace beats a half-alive service corrupting data.
A final working mindset
Java’s throwable model is strict for a reason. It separates expected failures from serious runtime damage. Good code respects that split.
Use exceptions to model failures your application can handle. Use runtime exceptions to expose broken assumptions. Treat errors as signals that the JVM or environment may no longer be safe. Above all, avoid lazy catch blocks. They save 10 seconds while coding and can cost hours during an incident.