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.