What is the difference between Callable and Runnable in Java?

Java Callable vs Runnable
Quick Answer: Runnable has a void run() method — no return value, no checked exceptions. Callable<V> has a V call() throws Exception method — it returns a result and can throw checked exceptions.

1. Short Answer

Both Runnable and Callable represent tasks that can be executed asynchronously, but they differ in two fundamental ways:

  • Return value: Runnable.run() returns void. Callable.call() returns a typed value V.
  • Exception handling: Runnable.run() cannot throw checked exceptions. Callable.call() declares throws Exception, so checked exceptions propagate to the caller via Future.get().

Both are functional interfaces and both can be submitted to an ExecutorService, but only Callable gives you a meaningful Future result.

2. Runnable Interface

Runnable is the original Java concurrency interface, present since Java 1.0. Its signature is simple:

@FunctionalInterface
public interface Runnable {
    void run(); // No return value, no checked exceptions
}

Key characteristics of Runnable:

  • The method returns void — the task produces no result.
  • No checked exceptions are declared — any checked exception must be caught inside run().
  • Can be passed directly to a Thread constructor or to ExecutorService.execute().
  • When submitted via ExecutorService.submit(Runnable), the returned Future<?> always returns null from get().

3. Callable Interface

Callable was introduced in Java 5 as part of java.util.concurrent to overcome Runnable's limitations. Its signature is:

@FunctionalInterface
public interface Callable {
    V call() throws Exception; // Returns a value, can throw checked exceptions
}

Key characteristics of Callable:

  • Generic — the type parameter V specifies the result type.
  • The method returns a value of type V.
  • Declares throws Exception — checked exceptions propagate naturally.
  • Must be submitted via ExecutorService.submit(Callable), which returns a Future<V>.
  • Cannot be passed directly to a Thread constructor (Thread only accepts Runnable).
Callable Cannot Start a Thread Directly

Unlike Runnable, you cannot do new Thread(callable).start(). Callable tasks must be submitted to an ExecutorService or wrapped with FutureTask.

4. Key Differences

Feature Runnable Callable<V>
Packagejava.langjava.util.concurrent
SinceJava 1.0Java 5
Method namerun()call()
Return typevoidV (generic)
Checked exceptionsNot allowedAllowed (throws Exception)
Thread constructorYes — new Thread(runnable)No
execute() supportYes — executor.execute(runnable)No
submit() returnFuture<?> (get() returns null)Future<V> (get() returns result)

5. Code Example

The following example shows both interfaces in action with an ExecutorService:

import java.util.concurrent.*;

public class CallableVsRunnableDemo {

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // --- Runnable: fire-and-forget, no result ---
        Runnable runnable = () -> {
            System.out.println("Runnable running on: " + Thread.currentThread().getName());
            // Must catch checked exceptions here — cannot declare them
        };

        Future runnableFuture = executor.submit(runnable);
        runnableFuture.get(); // Blocks until done; always returns null
        System.out.println("Runnable done. Result: " + runnableFuture.get()); // null

        // --- Callable: returns a result, can throw checked exceptions ---
        Callable callable = () -> {
            System.out.println("Callable running on: " + Thread.currentThread().getName());
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum; // Returns a real value
        };

        Future callableFuture = executor.submit(callable);
        Integer result = callableFuture.get(); // Blocks and retrieves the result
        System.out.println("Callable result (sum 1-100): " + result); // 5050

        // --- Callable with checked exception ---
        Callable riskyCallable = () -> {
            // Can throw checked exceptions — no try/catch needed here
            if (Math.random() > 0.5) {
                throw new Exception("Something went wrong in the task");
            }
            return "Success";
        };

        Future riskyFuture = executor.submit(riskyCallable);
        try {
            String value = riskyFuture.get(); // May throw ExecutionException
            System.out.println("Risky callable result: " + value);
        } catch (ExecutionException e) {
            // The checked exception is wrapped in ExecutionException
            System.out.println("Task failed: " + e.getCause().getMessage());
        }

        executor.shutdown();
    }
}
// Possible output:
// Runnable running on: pool-1-thread-1
// Runnable done. Result: null
// Callable running on: pool-1-thread-2
// Callable result (sum 1-100): 5050
// Task failed: Something went wrong in the task

6. Using ExecutorService: submit() vs execute()

6.1 execute() — Runnable only

ExecutorService.execute(Runnable) runs the task but returns nothing. Exceptions thrown inside are sent to the thread's UncaughtExceptionHandler and are not propagated to the caller.

executor.execute(() -> System.out.println("Fire and forget"));
// No return value, no way to check completion or catch exceptions

6.2 submit() — Runnable or Callable

ExecutorService.submit() accepts both. It always returns a Future:

// submit(Runnable) → Future
Future f1 = executor.submit(() -> System.out.println("Runnable"));
f1.get(); // null — but useful to know when the task finished

// submit(Callable) → Future
Future f2 = executor.submit(() -> "Hello from Callable");
String s = f2.get(); // "Hello from Callable"

// submit(Runnable, T result) → Future
// Lets you specify what get() returns for a Runnable
Future f3 = executor.submit(() -> System.out.println("Runnable"), "done");
String r = f3.get(); // "done"
Best Practice: Prefer submit() over execute(). The returned Future lets you detect task completion, retrieve results, and handle exceptions — even for Runnable tasks.

7. When to Use Each

Use Runnable when:

  • The task produces no result (e.g., logging, sending a notification, updating a UI element).
  • You are passing the task to a Thread constructor.
  • You want the simplest possible representation of a background task.

Use Callable when:

  • The task computes and returns a value (e.g., fetching data from a database, calling an API).
  • The task may throw a checked exception that the caller must handle.
  • You need to use Future to retrieve the result, check task status, or cancel the task.
  • You are submitting multiple tasks with invokeAll() or invokeAny(), which require Callable.

8. Why This Question Matters in Interviews

This is a standard concurrency question that tests whether you understand Java's task abstraction model. Strong answers should include:

  • The precise method signatures: void run() vs V call() throws Exception.
  • The fact that Callable was added in Java 5 with java.util.concurrent.
  • How exceptions behave differently: wrapped in ExecutionException for Callable.
  • The relationship between Callable, Future, and FutureTask.
  • Practical examples: use Runnable for background jobs, Callable for parallel computations that return results.

9. Conclusion

Runnable and Callable are both ways to represent asynchronous tasks in Java. Choose Runnable for simple fire-and-forget tasks that produce no result. Choose Callable when you need the task to return a computed value or propagate checked exceptions. Both can be submitted to an ExecutorService via submit(), but only Callable gives you a Future<V> with a meaningful return value. Understanding when to use each is a sign of solid concurrency knowledge in Java interviews.

Subscribe to Our Newsletter

Get the latest updates and exclusive content delivered to your inbox!