Java
Throwable
Exception
Try-Catch
Programming Concepts

Difference between using Throwable and Exception in a try catch

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Throwable catches everything: both Exception (recoverable problems) and Error (fatal JVM-level problems like OutOfMemoryError). Exception catches only the recoverable subset. In nearly all application code, you should catch Exception (or better, specific exception types). Catching Throwable is reserved for top-level error handlers, framework shutdown hooks, and thread pool executors where you genuinely need to prevent silent thread death.

The Java Exception Hierarchy

Understanding what to catch requires knowing the class hierarchy:

text
1java.lang.Object
2  └── java.lang.Throwable
3        ├── java.lang.Exception
4        │     ├── java.lang.RuntimeException    (unchecked)
5        │     │     ├── NullPointerException
6        │     │     ├── IllegalArgumentException
7        │     │     ├── IndexOutOfBoundsException
8        │     │     └── ...
9        │     ├── IOException                   (checked)
10        │     ├── SQLException                  (checked)
11        │     └── ...
12        └── java.lang.Error
13              ├── OutOfMemoryError
14              ├── StackOverflowError
15              ├── VirtualMachineError
16              ├── NoClassDefFoundError
17              └── ...

The catch clause matches the specified type and all of its subclasses. Catching Throwable matches the entire tree. Catching Exception matches everything except the Error branch.

Catching Exception: The Standard Practice

Catching Exception covers all problems that application code is expected to handle: checked exceptions (I/O failures, network errors, parse errors) and unchecked runtime exceptions (null references, illegal arguments, array bounds violations).

java
1public User loadUser(String id) {
2    try {
3        String json = httpClient.get("/users/" + id);
4        return objectMapper.readValue(json, User.class);
5    } catch (IOException e) {
6        // Network or parsing failure - recoverable
7        log.warn("Failed to load user {}: {}", id, e.getMessage());
8        return User.defaultUser();
9    } catch (Exception e) {
10        // Catch-all for unexpected runtime exceptions
11        log.error("Unexpected error loading user {}", id, e);
12        throw new ServiceException("User load failed", e);
13    }
14}

The pattern above catches the specific expected exception first (IOException), then uses a broader Exception catch as a safety net. This is the recommended approach for most application code.

Catching Throwable: When and Why

Catching Throwable includes Error subtypes. These represent conditions the JVM considers unrecoverable:

Error TypeMeaningShould you catch it?
OutOfMemoryErrorJVM heap is exhaustedAlmost never (state is corrupted)
StackOverflowErrorCall stack exceededRarely (indicates infinite recursion)
NoClassDefFoundErrorRequired class missing at runtimeSometimes (classpath issues in plugins)
VirtualMachineErrorJVM itself is failingNever (JVM is dying)
AssertionErrorAssertion failedRarely (indicates logic bugs)
LinkageErrorClass loading/linking failureSometimes (in classloader-heavy systems)

There are legitimate reasons to catch Throwable, but they are narrow:

1. Top-Level Thread Pool Error Handlers

java
1ExecutorService executor = Executors.newFixedThreadPool(4);
2executor.submit(() -> {
3    try {
4        processTask();
5    } catch (Throwable t) {
6        // Without this, the thread dies silently and the task is lost
7        log.error("Task failed with unrecoverable error", t);
8        // Do NOT try to "recover" - just log and let the thread return
9    }
10});

Without catching Throwable here, an OutOfMemoryError thrown inside the task kills the thread silently. The Future stores the error, but if nobody calls Future.get(), the failure is invisible.

2. Framework Shutdown Hooks

java
1Runtime.getRuntime().addShutdownHook(new Thread(() -> {
2    try {
3        gracefulShutdown();
4    } catch (Throwable t) {
5        // Log but do not rethrow - this is the last chance to record the error
6        System.err.println("Shutdown hook failed: " + t.getMessage());
7    }
8}));

3. Plugin or Classloader Isolation Boundaries

java
1public Object invokePlugin(String className, Object input) {
2    try {
3        Class<?> pluginClass = pluginClassLoader.loadClass(className);
4        Method method = pluginClass.getMethod("process", Object.class);
5        return method.invoke(pluginClass.getDeclaredConstructor().newInstance(), input);
6    } catch (Throwable t) {
7        // Plugin classloader issues can throw NoClassDefFoundError, LinkageError
8        log.error("Plugin {} failed", className, t);
9        return null;
10    }
11}

Side-by-Side Comparison

java
1// Catching Exception - standard application code
2try {
3    riskyOperation();
4} catch (Exception e) {
5    log.error("Operation failed", e);
6    // Can recover, retry, or fail gracefully
7    // Does NOT catch Error subtypes
8}
9
10// Catching Throwable - top-level safety net
11try {
12    riskyOperation();
13} catch (Throwable t) {
14    log.error("Operation failed with potentially fatal error", t);
15    // Catches EVERYTHING including OutOfMemoryError, StackOverflowError
16    // Recovery may not be possible - state could be corrupted
17}
Aspectcatch (Throwable t)catch (Exception e)
Catches checked exceptionsYesYes
Catches runtime exceptionsYesYes
Catches Error subtypesYesNo
Typical use caseFramework-level error handlingApplication-level error handling
Recovery expectationLow (log and fail)High (handle and continue)
Risk of masking fatal errorsHighLow
Best practice recommendationOnly at thread/process boundariesPreferred for business logic

The "Catch Specific" Principle

Both catch (Throwable) and catch (Exception) are broad catches. In most cases, you should catch the most specific exception type you expect.

java
1// Best: catch exactly what you expect
2try {
3    Path path = Path.of(filename);
4    String content = Files.readString(path);
5    return objectMapper.readValue(content, Config.class);
6} catch (NoSuchFileException e) {
7    log.info("Config file not found, using defaults");
8    return Config.defaults();
9} catch (JsonProcessingException e) {
10    throw new ConfigException("Invalid config format in " + filename, e);
11} catch (IOException e) {
12    throw new ConfigException("Failed to read config file " + filename, e);
13}
14
15// Acceptable: broad catch with re-throw of unknowns
16try {
17    complexOperation();
18} catch (SpecificBusinessException e) {
19    handleBusinessError(e);
20} catch (Exception e) {
21    // Log and re-throw - do not silently swallow
22    log.error("Unexpected exception in complexOperation", e);
23    throw e;
24}

Multi-Catch Syntax (Java 7+)

Java 7 introduced multi-catch, which is cleaner than catching a broad type when you want to handle several specific exceptions the same way.

java
1try {
2    parseAndStore(input);
3} catch (NumberFormatException | DateTimeParseException | JsonParseException e) {
4    // Handle all parsing failures the same way
5    log.warn("Invalid input format: {}", e.getMessage());
6    return ValidationResult.invalid(e.getMessage());
7}

This is preferable to catch (Exception e) because it explicitly documents which exceptions you expect, and it does not accidentally catch exceptions you did not anticipate.

What Happens If You Catch Throwable and An Error Occurs

When the JVM throws an OutOfMemoryError, catching it does not magically free memory. The application's state is likely corrupted: collections may be partially populated, transactions may be half-committed, and locks may be held indefinitely. Catching the error prevents the thread from terminating, but the application is in an unreliable state.

java
1// This is dangerous - the application may be in a corrupt state
2try {
3    List<byte[]> data = new ArrayList<>();
4    while (true) {
5        data.add(new byte[1_000_000]); // Allocate until OOM
6    }
7} catch (OutOfMemoryError e) {
8    // The list is partially populated, GC is under severe pressure
9    // Any subsequent allocation might trigger another OOM
10    log.error("Out of memory", e);
11    // "Recovering" here is unreliable
12}

The safe pattern for OutOfMemoryError is to catch it only at the outermost level (e.g., in a thread pool's error handler), log it, and shut down the application or reject the current request.

Common Pitfalls

  • Catching Throwable in application code and swallowing the error. This masks fatal JVM problems like OutOfMemoryError, making them invisible. The application continues in a corrupt state, producing wrong results or deadlocking. Always re-throw or terminate after catching Throwable.
  • Catching Exception and silently returning null. Even recoverable exceptions should be logged. Returning null silently pushes the failure downstream, where it appears as a NullPointerException with no indication of the root cause.
  • Catching Error to "recover" from it. StackOverflowError and OutOfMemoryError indicate that fundamental resources are exhausted. Recovery code is likely to trigger the same error again. Catch these only for logging and shutdown.
  • Using catch (Exception e) when you know the specific exceptions. Broad catches accidentally swallow unexpected exceptions, hiding bugs. Catch the narrowest type possible.
  • Forgetting that catch clauses are evaluated in order. If you catch Exception before a specific subtype, the specific catch block is unreachable. The compiler will flag this, but it is a common mistake during refactoring.
  • Not including the original exception in the re-thrown exception. Always chain exceptions: throw new ServiceException("message", e). Losing the cause makes debugging nearly impossible.

Summary

Catch Exception in application code where you expect to recover from failures. Catch Throwable only at thread boundaries, shutdown hooks, or framework-level error handlers where silent thread death would be worse than catching a potentially fatal error. In both cases, prefer catching the most specific exception types first. Never silently swallow exceptions of any type. When you do catch Throwable, treat it as a last-resort safety net: log the error and either re-throw it or initiate a controlled shutdown. The key principle is that catching broader types increases the risk of masking bugs, so default to the narrowest catch that handles your known failure modes.


Course illustration
Course illustration

All Rights Reserved.