Common Thread Exceptions in Java and How to Handle Them Effectively

Illustration for Common Thread Exceptions in Java and How to Handle Them Effectively
By Last updated:

In multithreaded Java applications, exceptions can cause subtle bugs, unexpected behavior, or system crashes if not handled properly. Unlike single-threaded environments, errors in threads may go unnoticed unless explicitly caught and logged.

This tutorial explains the most common exceptions encountered while working with threads in Java and how to mitigate them with robust patterns and practices.


Overview of Java Threads

A thread represents a path of execution in a Java program. Java threads can be created using the Thread class or implementing the Runnable/Callable interfaces. Multithreading improves responsiveness and performance but adds complexity to error handling.


Most Common Thread Exceptions

1. InterruptedException

When It Occurs

When a thread is blocked (e.g., via sleep(), join(), or wait()) and another thread interrupts it.

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // recommended
}

How to Handle

  • Always re-interrupt if catching it
  • Use Thread.currentThread().isInterrupted() to check status

2. IllegalThreadStateException

When It Occurs

  • Calling start() twice on the same thread
  • Incorrectly modifying thread states
Thread t = new Thread(() -> {});
t.start();
t.start(); // ❌ IllegalThreadStateException

How to Handle

  • Ensure start() is called only once
  • Use flags to check thread status if needed

3. RejectedExecutionException

When It Occurs

Submitted task cannot be accepted by ExecutorService.

ExecutorService executor = Executors.newFixedThreadPool(1);
executor.shutdown();
executor.submit(() -> {}); // ❌ RejectedExecutionException

How to Handle

  • Check executor.isShutdown() before submitting
  • Use custom RejectedExecutionHandler if needed

4. ThreadDeath

When It Occurs

When stop() is called on a thread (deprecated and dangerous).

t.stop(); // Not recommended

How to Handle

  • Avoid stop()
  • Use interrupt() and cooperative shutdown flags

5. ExecutionException

When It Occurs

When using Future.get() and the task threw an exception internally.

Future<?> future = executor.submit(() -> { throw new RuntimeException(); });
future.get(); // throws ExecutionException

How to Handle

  • Catch ExecutionException
  • Retrieve and handle cause using getCause()

6. TimeoutException

When It Occurs

When a blocking call on a future exceeds a specified timeout.

future.get(1, TimeUnit.SECONDS);

How to Handle

  • Use timeouts only when necessary
  • Provide fallback logic

Additional Runtime Exceptions in Threads

  • NullPointerException in thread logic
  • ArrayIndexOutOfBoundsException in shared collections
  • IllegalMonitorStateException with wait()/notify() outside synchronized blocks

Best Practices for Handling Thread Exceptions

  • Always log exceptions inside threads
  • Wrap thread logic in try-catch blocks
  • Use Thread.setUncaughtExceptionHandler() for fallback handling
Thread t = new Thread(() -> {
    throw new RuntimeException("Uncaught!");
});
t.setUncaughtExceptionHandler((thr, ex) -> {
    System.err.println("Error in " + thr.getName() + ": " + ex);
});
t.start();

Java Version Tracker

📌 What's New in Java Versions?

Java 8

  • CompletableFuture with async error handling
  • Lambda-friendly Runnable syntax

Java 9

  • Better diagnostics via StackWalker

Java 11+

  • CompletableFuture.exceptionally() support enhanced

Java 21

  • Virtual threads isolate failures better
  • Structured concurrency scopes allow collective error handling
  • Scoped values provide safer shared data

Real-World Scenarios

  • Thread pool exhausted → RejectedExecutionException
  • Forgetting to catch InterruptedException → zombie thread
  • Improper use of wait/notify → IllegalMonitorStateException

Common Pitfalls

  • Ignoring InterruptedException
  • Calling start() multiple times
  • Not checking executor.isShutdown()
  • Improperly terminating threads with stop()
  • Swallowing exceptions in catch blocks silently

Multithreading Design Patterns with Exception Handling

  • Worker Thread: Wrap worker tasks in try-catch
  • Future Task: Handle exceptions via get()
  • Thread-per-Message: Log failures centrally
  • Observer Pattern: Propagate error events

Conclusion and Key Takeaways

  • Thread exceptions can silently fail if unhandled
  • Each exception type indicates specific programming errors or lifecycle misuse
  • Always log and propagate exceptions responsibly
  • Use modern concurrency APIs and error-handling patterns for robustness

FAQs

Q1: Why doesn’t an exception crash the thread?
A: Uncaught exceptions terminate only the current thread, not the JVM.

Q2: What’s the safest way to stop a thread?
A: Use interrupt() and exit the loop if Thread.currentThread().isInterrupted() is true.

Q3: Can virtual threads throw InterruptedException?
A: Yes. They behave similarly to platform threads.

Q4: How to debug thread crashes?
A: Use logging, setUncaughtExceptionHandler(), or external profilers.

Q5: Are exceptions caught by ExecutorService?
A: No. You must call Future.get() to check for internal failures.

Q6: How do I stop threads in ExecutorService?
A: Call shutdown() and awaitTermination().

Q7: Should I catch ThreadDeath?
A: No. stop() is deprecated, and catching ThreadDeath is strongly discouraged.

Q8: Can I restart a failed thread?
A: No. Create a new thread instead.

Q9: How do I handle exceptions in CompletableFuture?
A: Use .exceptionally(), .handle(), or .whenComplete().

Q10: Is InterruptedException checked or unchecked?
A: It’s a checked exception and must be declared or caught.