Q22 of 40 · Core Java

What is the difference between Runnable and Callable?

Core JavaMidrunnablecallableexecutorservicefutureconcurrency

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:

  1. It returns a value — you get the result back via Future<V>.get().
  2. It can throw checked exceptions — ExecutorService.submit(Callable) wraps them in ExecutionException, which you unwrap at the Future.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();

// WHAT INTERVIEWERS LOOK FOR

The two concrete differences (return type and checked exception), Future<V> and its blocking get(), and the ExecutionException wrapping pattern. Strong answers note the timeout overload and CompletableFuture as the modern successor.

// COMMON PITFALL

Not mentioning that ExecutionException wraps the original exception. When future.get() throws, the cause is the original exception from call() — callers must unwrap it via getCause(), which is easy to forget and leads to generic error messages.