Q22 of 40 · Core Java
What is the difference between Runnable and Callable?
Short answer
Short answer: Runnable.run() returns void and cannot throw checked exceptions — it's fire-and-forget. Callable<V>.call() returns a value of type V and can throw checked exceptions. Callable works with ExecutorService.submit() which returns a Future<V> to retrieve the result or exception.
Detail
Both represent units of work that can be executed by a thread or thread pool, but they serve different purposes.
Runnable is a functional interface with void run(). The caller gets no result back and cannot observe a checked exception from the task (unchecked exceptions still propagate through the thread, but they're lost unless the thread's UncaughtExceptionHandler is set). Used with Thread, ExecutorService.execute(), and anywhere you need fire-and-forget work.
Callable<V> is a functional interface with V call() throws Exception. It was added specifically to address Runnable's two limitations:
- It returns a value — you get the result back via
Future<V>.get(). - It can throw checked exceptions —
ExecutorService.submit(Callable)wraps them inExecutionException, which you unwrap at theFuture.get()call site.
Future<V>: ExecutorService.submit(callable) returns a Future<V>. Calling future.get() blocks until the task completes and returns the value or re-throws any exception as ExecutionException. future.get(timeout, unit) adds a deadline — it throws TimeoutException if the task hasn't finished.
For test automation: Callable + ExecutorService is the right pattern for parallel API calls where you need results back — e.g., firing multiple requests concurrently and collecting their responses. CompletableFuture (Java 8+) is the modern alternative with richer composition, but Callable/Future is still widely used and likely to appear in legacy test frameworks.
// EXAMPLE
RunnableVsCallable.java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// Runnable — no return value, no checked exception
Runnable warmup = () -> {
hitEndpoint("/health");
System.out.println("warmup done");
};
new Thread(warmup).start(); // fire and forget
// Callable — returns value, can throw
Callable<Integer> countActiveUsers = () -> {
var response = httpClient.get("/api/users?active=true");
return response.body().path("total").asInt();
// can throw IOException here — caller handles it
};
ExecutorService pool = Executors.newFixedThreadPool(4);
// submit() returns Future<Integer>
Future<Integer> future = pool.submit(countActiveUsers);
// blocking get — waits for result, unwraps exceptions
try {
int count = future.get(5, TimeUnit.SECONDS); // blocks up to 5s
System.out.println("Active users: " + count);
} catch (TimeoutException e) {
future.cancel(true); // interrupt the task
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // the original exception from call()
log.error("Task failed", cause);
}
pool.shutdown();