Back to Thread Pool & Executor Framework
Q3Question 3 of 8

Callable vs Runnable?

Key Concepts
  • Runnable has void run() with no return value and no checked exceptions.
  • Callable<V> has V call() throws Exception, returning a result and allowing checked exceptions.
  • Submit a Callable to ExecutorService to get a Future<V> for retrieving the result later.
  • Future.get() blocks until the result is ready and throws ExecutionException if the task failed.
  • Use Runnable for fire-and-forget tasks like notifications; use Callable when you need the result.
  • Executors.callable(Runnable, result) converts a Runnable into a Callable with a preset return value.

Definition

Runnable defines void run() with no return value and no checked exceptions, while Callable<V> defines V call() throws Exception, returning a result through a Future<V> when submitted to an ExecutorService.
  • Runnable – a functional interface with void run() that executes a task returning nothing and cannot declare checked exceptions.
  • Callable – a functional interface with V call() throws Exception that executes a task, returns a result of type V, and can throw any checked exception.
  • Future – the handle returned by submit(Callable) that lets you retrieve the Callable's result with get(), which blocks until the value is ready.
  • Executors.callable() – a utility method that wraps a Runnable into a Callable, with an optional preset return value.
  • Exception propagation – Callable exceptions are captured in the Future and surface as ExecutionException when get() is called.

At National Bank of India, Runnable is a task form that says "Send an SMS to Rajesh about his trade settlement." No receipt, no response expected. Priya hands it to the teller and walks away. Callable is a form that says "Calculate Rajesh's portfolio value and give me the number." Priya gets a receipt (Future) and comes back later to collect the result: Rs.15,00,000. If the calculation failed, the receipt tells her why.

Understanding the Concepts

What is the difference between Callable and Runnable?

Both Runnable and Callable represent tasks that can be executed by threads. The fundamental difference is that Callable returns a result and can throw checked exceptions, while Runnable does neither.

| Feature | Runnable | Callable |
|---|---|---|
| Method signature | void run() | V call() throws Exception |
| Return value | None | V (any type) |
| Checked exceptions | Cannot throw | Can throw any Exception |
| Functional interface | Yes (since Java 8) | Yes (since Java 8) |
| Thread constructor | Accepted | Not accepted |
| submit() return | Future (get() returns null) | Future (get() returns V) |

JAVA
package com.bank.service;

import java.util.concurrent.*;
import java.math.BigDecimal;

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

        // Runnable: no return, no checked exception
        Runnable notification = () -> {
            System.out.println("SMS sent to Rajesh: Trade TRD-001 settled");
        };
        pool.execute(notification);

        // Callable: returns BigDecimal, throws checked exception
        Callable<BigDecimal> navCalc = () -> {
            BigDecimal nav = new BigDecimal("1500000.00");
            if (nav.compareTo(BigDecimal.ZERO) <= 0) {
                throw new Exception("NAV calculation failed for FUND-001");
            }
            return nav;
        };
        Future<BigDecimal> future = pool.submit(navCalc);
        System.out.println("NAV: Rs." + future.get());

        pool.shutdown();
    }
}
TEXT
SMS sent to Rajesh: Trade TRD-001 settled
NAV: Rs.1500000.00

Why can Runnable not throw checked exceptions?

Runnable's run() method has no throws clause in its signature. This means any lambda or implementation of Runnable cannot declare checked exceptions. Only unchecked exceptions (RuntimeException and its subclasses) can escape from run(). If you need to handle IO failures or SQL errors inside a Runnable, you must catch them inside the method body.

JAVA
// WRONG: compile error, run() has no throws clause
Runnable bad = () -> {
    // throw new IOException("File not found"); // COMPILE ERROR
};

// Workaround: catch inside run()
Runnable workaround = () -> {
    try {
        java.nio.file.Files.readString(java.nio.file.Path.of("accounts.csv"));
    } catch (java.io.IOException e) {
        throw new RuntimeException("Failed to read accounts", e);
    }
};

// Callable: checked exceptions are fine
Callable<String> clean = () -> {
    return java.nio.file.Files.readString(java.nio.file.Path.of("accounts.csv"));
    // IOException propagates naturally through call()
};

How do you retrieve the result from a Callable?

Submit the Callable to an ExecutorService using submit(). This returns a Future that holds the pending result. Call future.get() to block until the result is ready, or future.get(timeout, unit) to block with a deadline.

JAVA
package com.bank.service;

import java.util.concurrent.*;
import java.math.BigDecimal;

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

        Future<BigDecimal> balanceFuture = pool.submit(() -> {
            Thread.sleep(200); // simulate DB call
            return new BigDecimal("250000.50");
        });

        // Do other work while the balance is being fetched
        System.out.println("Preparing statement...");

        // Now block to get the result
        BigDecimal balance = balanceFuture.get(5, TimeUnit.SECONDS);
        System.out.println("Account SAV2001 balance: Rs." + balance);

        pool.shutdown();
    }
}
TEXT
Preparing statement...
Account SAV2001 balance: Rs.250000.50

How does exception handling differ between Runnable and Callable?

With execute(Runnable), if the task throws an unchecked exception, it goes to the thread's UncaughtExceptionHandler. The worker thread dies and the pool creates a replacement. The exception may be silently lost if no handler is configured.

With submit(Callable), any exception is captured inside the Future. The worker thread survives. The exception surfaces only when future.get() is called, wrapped in an ExecutionException.

JAVA
package com.bank.service;

import java.util.concurrent.*;

public class ExceptionHandling {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // submit(): exception captured in Future
        Future<?> future = pool.submit(() -> {
            throw new RuntimeException("Settlement failed for TRD-002");
        });

        try {
            future.get();
        } catch (ExecutionException e) {
            // Unwrap to get the original exception
            System.out.println("Cause: " + e.getCause().getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        pool.shutdown();
    }
}
TEXT
Cause: Settlement failed for TRD-002

How do you convert a Runnable to a Callable?

The Executors utility class provides callable(Runnable) and callable(Runnable, T result) to wrap a Runnable as a Callable.

JAVA
Runnable task = () -> System.out.println("Audit log written");

Callable<Object> c1 = Executors.callable(task);          // get() returns null
Callable<String> c2 = Executors.callable(task, "DONE");  // get() returns "DONE"

When should you use Runnable vs Callable?

Use Runnable for fire-and-forget work where you do not need the result: sending notifications, writing audit logs, warming caches, and cleanup tasks.

Use Callable when you need the computation result: balance lookups, interest calculations, credit score fetches, API calls, and any task where the caller needs to know the outcome.

  • EOD batch notifications (Runnable) – send 10,000 SMS messages, no result needed.
  • EOD interest calculation (Callable) – compute interest for 50,000 accounts, collect each amount.
  • Statement generation (Callable) – generate PDF, return file path.
  • Cache refresh (Runnable) – reload price data in background.

What about thread safety when sharing data between tasks?

Both Runnable and Callable tasks run on pool threads. If multiple tasks share mutable state, synchronization is required. Use BigDecimal for money calculations, ConcurrentHashMap for shared maps, and AtomicLong for counters.

What are common interview follow-up questions?

  • Can you use Callable with new Thread()? No, Thread's constructor only accepts Runnable.
  • What does Future.get() return when you submit a Runnable? It returns null.
  • How do you handle timeouts when waiting for a Callable result? Use get(timeout, unit) which throws TimeoutException.
  • Can a Callable return void? Use Callable and return null from call().

Visual Overview

DIAGRAM
  Runnable                          Callable<V>
  ┌──────────────────┐              ┌──────────────────────────┐
  │ void run()       │              │ V call() throws Exception │
  │ No return        │              │ Returns V                 │
  │ No checked exc   │              │ Checked exceptions OK     │
  └────────┬─────────┘              └────────────┬─────────────┘
           │                                     │
           ▼                                     ▼
  pool.execute(runnable)            pool.submit(callable)
           │                                     │
           ▼                                     ▼
  void (fire-and-forget)            Future<V>
                                         │
                                         ▼
                                    future.get() → V
                                    (blocks until ready)

Key Takeaway

Runnable and Callable are the two task abstractions in Java concurrency. Runnable has void run() and is for fire-and-forget work: send a notification, write an audit log, warm a cache. Callable has V call() throws Exception and is for compute-and-return work: calculate a balance, fetch a credit score, generate a report.

When you submit a Callable to an ExecutorService, you get a Future that holds the pending result. Calling future.get() blocks until the value is ready. If the task threw an exception, get() wraps it in an ExecutionException so the caller can handle it. Runnable exceptions via execute() go to UncaughtExceptionHandler and may be lost.

In our banking systems, the rule is straightforward. Need the answer? Callable. Do not need the answer? Runnable. Interest calculation for 50,000 accounts: submit Callable tasks, collect BigDecimal results via invokeAll. SMS notifications: submit Runnable tasks via execute, move on.

Remember one sentence: Runnable runs and forgets, Callable calls and returns.

---

Share
Use ← → arrow keys to navigate