Runnable vs Callable in Java
Fast recall
Quick revision
Core answer
Awareness of both concurrent task interfaces and ability to choose the right abstraction based on requirements
Not knowing Callable can throw checked exceptions, which is a key advantage when tasks involve I/O or database operations
Demonstrate the Future.get pattern for retrieving Callable results and explain how ExecutorService.submit returns Future for both
Interview-ready guide
Interview answer
The main difference is what they return and how they handle exceptions.
Runnable has a run method that returns void. It's for tasks that just do work without a result. It also can't throw checked exceptions, so an IOException has to be caught inside run.
Callable has a call method that returns a value, whatever type you set with the generic parameter. And it can throw checked exceptions directly, which is great for database or file work. When you submit a Callable to an ExecutorService, you get back a Future.
You call get on the Future to retrieve the result, and it blocks until the task finishes. get throws an ExecutionException wrapping it.
In practice, use Runnable for fire-and-forget tasks like sending emails or logging. Use Callable when you need a computed result, like fetching data from an API. The exception-handling difference is the one that stands out, so mention it if the task involves I/O or databases.
Runnable vs Callable in Java
Direct Answer
Runnable has a run method that returns void and cannot throw checked exceptions. Callable has a call method that returns a value of type V and can throw checked exceptions. Use Callable when you need a result from the task or need to propagate checked exceptions.
Code Example
import java.util.concurrent.*;
// Runnable - no return value, catches exceptions internally
class NotificationTask implements Runnable {
private final String userId;
NotificationTask(String userId) { this.userId = userId; }
@Override
public void run() {
System.out.println("Notification sent to " + userId);
}
}
// Callable - returns a result, can throw checked exceptions
class UserFetchTask implements Callable<String> {
private final long userId;
UserFetchTask(long userId) { this.userId = userId; }
@Override
public String call() throws Exception { // checked exceptions allowed
return "User-" + userId; // returns the result
}
}
public class Demo {
public static void main(String[] args) throws Exception {
var executor = Executors.newFixedThreadPool(3);
executor.execute(new NotificationTask("user123")); // fire and forget
Future<String> future = executor.submit(new UserFetchTask(100L));
String name = future.get(); // blocks until the result is ready
System.out.println("Fetched: " + name);
executor.shutdown();
}
}
Common Mistakes & Pitfalls
- Forcing try-catch into Runnable: Wrapping everything in
try-catchinsiderun()when the task can fail signals you should be usingCallableto propagate the exception. - Forgetting Future.get blocks:
future.get()blocks the calling thread until the result is ready. Calling it too early stalls the main thread on a slow task. - Using Runnable when a result is needed: Stashing results in shared fields instead of returning them from
Callableadds needless thread-safety risk. - Not knowing submit takes both:
submitaccepts aRunnabletoo—it just returns aFuturewhoseget()returns null after normal completion.
Check your understanding
Answer these without looking back at the article.
- What happens when you call Future.get() and the Callable threw an exception?
- Can you use a Callable with a raw Thread?
- What is ExecutionException?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.