Runnable & Callable
Runnable and Callable are the two functional interfaces Java uses to hand a unit of work to a thread — one fire-and-forget, the other able to return a value and throw checked exceptions.
Before a thread (or an executor) can run anything, the work itself has to be packaged as an object. Java gives you two standard shapes for that package: java.lang.Runnable, which represents a task that takes no arguments and returns nothing, and java.util.concurrent.Callable<V>, which represents a task that returns a value of type V and may throw a checked exception. Understanding the difference — and when each is the right tool — is the foundation for everything else in the concurrency topics, since Thread, ExecutorService, and the Future/CompletableFuture APIs are all built around submitting one of these two task shapes.
Runnable: The Original Task Abstraction
Runnable is a functional interface with a single abstract method, run(), that takes no parameters and returns void. It has existed since Java 1.0 and is the classic way to describe 'a block of code to execute,' independent of any particular thread. A Runnable does not run itself — it is passed to something that will run it, most commonly a new Thread or an ExecutorService.
The entire contract: no arguments in, no value out, and the signature declares no checked exceptions.
Since Java 8, Runnable is a functional interface, so a lambda expression can implement it directly instead of writing an anonymous class.
public interface Runnable {
void run();
}Runnable task = () -> System.out.println("Running in: " + Thread.currentThread().getName());
Thread t = new Thread(task);
t.start();Callable: A Task That Returns a Value
java.util.concurrent.Callable<V> was introduced in Java 5 alongside the rest of java.util.concurrent. It is also a functional interface, but its single abstract method is call(), which returns a value of type V and is declared to throw Exception.
Compare this to Runnable's run(): Callable adds a return type and permits throwing a checked exception.
A Callable<Integer> that computes and returns a result, rather than just performing a side effect.
public interface Callable<V> {
V call() throws Exception;
}Callable<Integer> sumTask = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum;
};Why Callable Exists: The Two Gaps Runnable Left
Runnable predates java.util.concurrent by almost a decade and was designed around Thread, which has no built-in way to retrieve a result or propagate a checked exception from the code that ran on it. Two concrete gaps motivated Callable:
1) Return values. Runnable.run() returns void, so a Runnable can only communicate a result through side effects — writing to a shared field, a queue, or a callback — which invites the exact synchronization problems this topic group is about. Callable.call() returns V directly, and that value is what an ExecutorService hands back through a Future<V>.
2) Checked exceptions. Runnable.run() has no throws clause, so a lambda or method implementing it cannot throw a checked exception without catching and wrapping it first (commonly into an unchecked RuntimeException). Callable.call() is declared throws Exception, so checked exceptions propagate naturally and are surfaced later, wrapped in an ExecutionException, when code calls Future.get().
Submitting Runnable vs. Callable to an ExecutorService
Both interfaces exist mainly to be handed to something that executes tasks. Thread only accepts Runnable in its constructor. ExecutorService is more flexible: it accepts both, via overloaded submit() methods, and the return type of submit() tells you which kind of task you gave it.
submit(Runnable) returns a Future<?> whose get() always yields null on success; submit(Callable<V>) returns a Future<V> whose get() yields the task's actual result. ExecutorService and Future/CompletableFuture are covered in their own topics.
ExecutorService also offers submit(Runnable, T result) — run a Runnable but complete its Future with a caller-supplied fixed value, a small bridge for when you want a Future-shaped handle around Runnable-style work.
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<?> f1 = pool.submit(() -> System.out.println("runnable task")); // Runnable overload
Future<Integer> f2 = pool.submit(() -> 42); // Callable overload
System.out.println(f2.get()); // 42
pool.shutdown();Wrapping a Runnable's Checked-Exception Problem
Because Runnable.run() cannot declare checked exceptions, code that calls a checked-exception-throwing API from inside a Runnable must handle it locally — typically by catching it and either logging it, ignoring it (rarely correct), or rethrowing it wrapped in an unchecked exception.
UncheckedIOException (and RuntimeException generally) is the idiomatic way to smuggle a checked exception out of a Runnable-shaped API.
A Callable sidesteps this entirely: the same file-reading code can simply declare or propagate IOException, since call() already permits any Exception.
Runnable task = () -> {
try {
Files.readString(Path.of("config.txt"));
} catch (IOException e) {
throw new UncheckedIOException(e); // wrap: Runnable can't declare IOException
}
};Converting Between Runnable and Callable
java.util.concurrent.Executors provides adapter methods for moving between the two shapes when an API expects one but you have the other.
Executors.callable() wraps a Runnable so it can be submitted anywhere a Callable is expected, useful for uniform task-handling code paths.
There is no built-in Callable-to-Runnable adapter, because a Callable's return value and checked exception have nowhere to go inside run()'s signature — you would have to write that adapter yourself and decide how to discard the value and handle the exception.
Runnable r = () -> System.out.println("side effect only");
Callable<Object> c1 = Executors.callable(r); // Runnable -> Callable, result is always null
Callable<String> c2 = Executors.callable(r, "done"); // Runnable -> Callable, fixed result "done"Task Abstraction: Decoupling What From How
The deeper point of Runnable and Callable is separation of concerns: the task describes what work to do, while the executing mechanism (a raw Thread, a thread pool, a scheduled executor) decides how and when to run it — which thread, whether to queue it, whether to retry it. This is what lets the same Callable<Integer> be run directly, run inside a fixed thread pool, or scheduled to run later, without changing a line of the task's own code.
call() can even be invoked directly like an ordinary method when no concurrency is needed — Callable does not require an executor.
Callable<Integer> squareOf21 = () -> 21 * 2;
// Same task, three different execution strategies:
int direct = squareOf21.call(); // run synchronously, on this thread
Future<Integer> viaPool = pool.submit(squareOf21); // run asynchronously, on a pool thread
// ScheduledExecutorService could instead run it after a delayCommon Pitfall: run() Swallows Exceptions Silently in Threads
If a Runnable running directly on a plain Thread (not via an ExecutorService) throws an unchecked exception, that exception does not propagate to the code that called Thread.start() — it terminates only that thread, and by default is printed to standard error via the thread's uncaught exception handler. Code that needs to know a background Runnable failed must use a mechanism like Thread.setUncaughtExceptionHandler, or better, use an ExecutorService and inspect the resulting Future.
Without an explicit handler, the default behavior just prints the stack trace to System.err — silent from the perspective of the code that started the thread.
Thread t = new Thread(() -> { throw new RuntimeException("boom"); });
t.setUncaughtExceptionHandler((thread, ex) ->
System.err.println("Thread " + thread.getName() + " failed: " + ex));
t.start();Quick Reference
Runnable: void run(), no checked exceptions, since Java 1.0, used for Thread and fire-and-forget executor tasks. • Callable<V>: V call() throws Exception, since Java 5, used with ExecutorService.submit() to get a Future<V> back. • Both are functional interfaces (Java 8+) usable as lambdas. • Executors.callable(Runnable[, result]) adapts a Runnable into a Callable.
Key Takeaways
Runnable.run() takes no arguments, returns void, and cannot declare checked exceptions.
Callable<V>.call() returns a value of type V and is declared to throw Exception, so checked exceptions propagate naturally.
ExecutorService.submit() accepts both; a Runnable yields Future<?>, a Callable<V> yields Future<V>.
Executors.callable() adapts a Runnable into a Callable when an API requires the latter.
Both interfaces separate the description of work from the mechanism that executes it, which is what makes thread pools and schedulers reusable.
Runnable r = () -> { throw new IOException("fail"); }; // won't compile
Callable<Void> c = () -> { throw new IOException("fail"); }; // compiles
Runnable.run() cannot declare or propagate a checked exception; Callable.call() can, since it is declared throws Exception.
pool.submit((Runnable) () -> computeResult()); // result discarded
Future<Integer> f = pool.submit((Callable<Integer>) () -> computeResult());
Submitting work as a Runnable when you actually need the result back throws the value away — use a Callable so the Future carries it.
new Thread(runnableThatThrows).start(); // exception vanishes into stderr
ExecutorService pool = Executors.newSingleThreadExecutor(); Future<?> f = pool.submit(runnableThatThrows); f.get(); // rethrows as ExecutionException
A raw Thread's uncaught exception is not visible to the caller; submitting through an ExecutorService and calling get() surfaces the failure.
What are the two methods declared by Runnable and Callable, and how do their signatures differ?
Runnable declares void run() with no throws clause. Callable<V> declares V call() throws Exception — it returns a value and can propagate any checked exception.
Why was Callable added in Java 5 when Runnable already existed?
Runnable cannot return a value or declare checked exceptions, which made it awkward for tasks that compute a result or call APIs that throw checked exceptions. Callable<V> fills both gaps, working together with Future<V> to retrieve results and propagate failures asynchronously.
If you submit a Runnable to an ExecutorService, what type does submit() return, and what does calling get() on it produce?
It returns Future<?>, and get() blocks until the task finishes then returns null (or throws ExecutionException if the Runnable threw).
How can a checked exception thrown inside a Runnable be surfaced without changing Runnable's signature?
Catch it inside run() and rethrow it wrapped in an unchecked exception (e.g. RuntimeException or UncheckedIOException), or use Executors.callable(...) style patterns; alternatively, use a Callable instead of a Runnable when checked exceptions need to propagate directly.
What does Executors.callable(Runnable task, T result) do?
It adapts a Runnable into a Callable<T> whose call() runs the Runnable and then returns the fixed result value supplied by the caller, useful when an API requires a Callable but the underlying work is Runnable-shaped.
Is it valid to call Callable.call() directly, without any thread or executor involved?
Yes — call() is an ordinary method; invoking it directly runs the task synchronously on the calling thread, just like calling any other method. Concurrency only enters the picture once the Callable is handed to a Thread-based or executor-based runner.
Write a Callable<Integer> that computes the factorial of a given int n, and show submitting it to an ExecutorService and retrieving the result.
import java.util.concurrent.*; public class FactorialTask { public static void main(String[] args) throws Exception { int n = 5; Callable<Integer> factorial = () -> { int result = 1; for (int i = 2; i <= n; i++) result *= i; return result; }; ExecutorService pool = Executors.newSingleThreadExecutor(); Future<Integer> future = pool.submit(factorial); System.out.println("5! = " + future.get()); pool.shutdown(); } }
Prints '5! = 120'. The Callable captures n from the enclosing scope, submit() schedules it on the pool, and future.get() blocks until the pool thread finishes and returns the computed value.
What happens when this runs — does 'main continues' print, and what happens to the ArithmeticException?
"main continues" prints (the main thread does not wait for t and is unaffected by t's exception). The new thread throws ArithmeticException inside run(); since nothing catches it and no uncaught exception handler is set, the default handler prints a stack trace to System.err from that thread, but the exception never reaches main — it is not propagated back to the caller of start().
Write a method that accepts a Callable<String> and safely runs it, returning an Optional<String> that is empty if the Callable threw any exception.
import java.util.Optional; import java.util.concurrent.Callable; public class SafeRunner { static Optional<String> runSafely(Callable<String> task) { try { return Optional.ofNullable(task.call()); } catch (Exception e) { return Optional.empty(); } } public static void main(String[] args) { System.out.println(runSafely(() -> "ok")); // Optional[ok] System.out.println(runSafely(() -> { throw new Exception("boom"); })); // Optional.empty } } runSafely relies on Callable.call() being declared 'throws Exception' to catch any checked or unchecked failure in one catch block — something a Runnable-based version could not do as cleanly, since a Runnable would already have had to wrap any checked exception before reaching this method.
Runnable r = () -> {
int x = 10 / 0;
};
Thread t = new Thread(r);
t.start();
System.out.println("main continues");