Java Concurrency
What is the difference between Callable and Runnable?
Runnable represents work that runs without returning a result. Callable represents work that can return a result and throw checked exceptions, usually accessed through a Future.
The Short Answer
Runnable is for a task that performs work but does not return a result.
Callable is for a task that performs work and returns a result, or throws a checked exception.
The Real Problem
Imagine submitting 100 background tasks to check inventory, price products, call services, or process files.
Sometimes you only care that the task runs. Other times, you need the answer back. That is the core difference.
Runnable
Good for fire-and-forget work like logging, cleanup, or sending a notification.
Callable
Good when the caller needs the result, status, or failure of the background task.
Runnable Example
Runnable task = () -> {
System.out.println("Sending email...");
};
executorService.submit(task);This task runs, but there is no meaningful return value. If the task completes successfully, the returned Future does not contain a useful business result.
Callable Example
Callable<Integer> task = () -> {
return calculateScore();
};
Future<Integer> future = executorService.submit(task);
Integer score = future.get();Here the task produces an Integer. The Future acts like a handle to the result that will be available later.
The Mental Model
Why Future Matters
When you submit a Callable to an ExecutorService, the result is not available immediately. The task may still be running on another thread.
A Future lets you check, wait, cancel, or retrieve the result once the task completes.
Future<String> future = executorService.submit(() -> {
return fetchUserProfile(userId);
});
if (future.isDone()) {
String profile = future.get();
}Be careful: calling get() can block the current thread until the result is ready.
Important Interview Detail: Checked Exceptions
Runnable defines:
public interface Runnable {
void run();
}Notice that run() does not declare throws Exception.
Callable defines:
public interface Callable<V> {
V call() throws Exception;
}Notice that call() explicitly allows checked exceptions through throws Exception.
Runnable's run()method cannot throw checked exceptions directly. Callable's call() method can.
Callable<String> task = () -> {
return readFileFromDisk(); // can throw checked exception
};This makes Callable more natural for tasks like file reads, network calls, database lookups, or service calls where failure is part of the contract.
Common Mistake
A common mistake is using Runnable for a task where the caller later needs the result.
// Awkward: where does the result go?
Runnable task = () -> {
int value = calculateScore();
};Use Callable instead:
Callable<Integer> task = () -> calculateScore();When Should You Use Each?
Use Runnable when...
- You do not need a return value
- The task is fire-and-forget
- You are doing logging, cleanup, or notification work
Use Callable when...
- You need a result
- You need exception handling through Future
- You are submitting work to an ExecutorService
Common Interview Follow-Ups
Can Runnable return a value?
Not directly. Runnable's run method returns void. You can mutate shared state, but that is usually less clean than using Callable.
Can Callable be used with ExecutorService?
Yes. Callable is commonly submitted to ExecutorService, which returns a Future holding the eventual result.
Does Future.get() block?
Yes. If the task has not completed yet, get() waits until the result is available or an exception occurs.
Is Callable a replacement for Runnable?
No. Callable is useful when you need a result or checked exception support. Runnable is still perfect for simple fire-and-forget work.