When submitting tasks to an ExecutorService, how do execute(Runnable) and submit(...) differ, and what does submit return?
answer
- execute = void, fire-and-forget; submit = returns Future
- submit overloads: Runnable, Runnable+result, Callable
- submit captures exceptions → surface only via get() as ExecutionException
- Ignoring the Future silently swallows failures
- submit wraps work in FutureTask (Runnable + Future)
basics
~20 sexecute(Runnable) just runs the task and returns nothing. submit(...) accepts a Runnable or Callable and returns a Future you can use to wait for completion, get a Callable's result, or cancel the task. submit also captures exceptions inside the Future instead of letting them escape.
solid answer
~40 sexecute(Runnable) comes from the Executor interface: it fires off a task and returns void, so there is no handle to its outcome — any exception propagates to the thread's uncaught-exception handler. submit(...) is on ExecutorService and is overloaded for Runnable, Runnable+result, and Callable; it always returns a Future<T>. Through that Future you can block for completion via get(), retrieve a Callable's return value, or cancel(). Crucially, exceptions thrown by a submitted task are captured and re-thrown wrapped in an ExecutionException when you call get() — they do not surface on their own. A common bug is submitting work and never inspecting the Future, which silently swallows failures. Choose execute for fire-and-forget Runnables where you don't need a result; choose submit when you need the result, completion signal, cancellation, or visible error handling.
code
java · 17 linesExecutorService pool = Executors.newFixedThreadPool(2);
// execute: fire-and-forget Runnable, no handle, no captured exception
pool.execute(() -> System.out.println("side effect"));
// submit a Callable: get a Future, retrieve the result
Future<Integer> f = pool.submit(() -> 21 * 2);
try {
int answer = f.get(); // blocks until done
System.out.println(answer); // 42
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (ExecutionException ee) {
Throwable cause = ee.getCause(); // the task's real exception
}
pool.shutdown();go deeper
Knows execute returns nothing and submit gives back a Future used to get a result.
Articulates all three submit overloads, that submit returns a Future immediately (non-blocking), and that exceptions are captured in the Future rather than thrown on their own.
Explains the exception-swallowing pitfall, the ExecutionException wrapping, when to prefer execute vs submit, and that submit backs the task with a FutureTask.
Discusses observability and error-handling policy across a codebase (e.g. mandating Future inspection or wrapping tasks to log failures), and how this informs API choices and pool configuration for resilience.
## Background: the two interfaces Java's task-running API is layered: - **`Executor`** is the minimal interface, with one method: `void execute(Runnable command)`. It says only "run this command somehow." - **`ExecutorService`** extends `Executor` and adds lifecycle (`shutdown`) and the richer **`submit`** methods plus bulk operations (`invokeAll`, `invokeAny`). Both consume **command objects** (Runnable/Callable) — this is the Command pattern. The difference is what you get back. ## execute(Runnable) ```java executor.execute(() -> doWork()); ``` - Accepts only a **Runnable** (no Callable, because there is no way to return a result). - Returns **void** — you have no handle to the task. - If the task throws, the exception is **not** captured anywhere you can see it; it propagates on the worker thread and reaches that thread's `UncaughtExceptionHandler` (and, in a ThreadPoolExecutor, may trigger replacement of the worker thread). - Best for **fire-and-forget** work where you neither need a result nor need to know if it failed. ## submit(...) `submit` is overloaded three ways: ```java Future<?> f1 = executor.submit(runnable); // result is null Future<String> f2 = executor.submit(runnable, "done"); // result is the given value Future<Integer> f3 = executor.submit(() -> 2 + 2); // Callable: result is its return ``` All return a **`Future<T>`**, a handle to the eventual outcome. With it you can: - **`get()`** — block until the task finishes and return its result (or null for a Runnable). - **`get(timeout, unit)`** — block with a deadline. - **`cancel(mayInterruptIfRunning)`** — attempt to cancel. - **`isDone()` / `isCancelled()`** — poll status. ## The exception difference (the important gotcha) With `execute`, an exception escapes to the uncaught-exception handler. With `submit`, the exception is **captured inside the Future**. It stays hidden until you call `get()`, which then throws an **`ExecutionException`** whose `getCause()` is the original throwable: ```java Future<?> f = executor.submit(() -> { throw new IllegalStateException("boom"); }); try { f.get(); } catch (ExecutionException e) { Throwable original = e.getCause(); // IllegalStateException("boom") } ``` This leads to a classic bug: code that calls `submit` but never calls `get()` on the returned Future will **silently swallow** any failure — the task fails, nothing is logged, nothing throws. If you do fire-and-forget but want failures visible, prefer `execute`, or always inspect the Future. ## How submit adapts a Runnable Internally, `submit` wraps the Runnable/Callable in a **`FutureTask`**, which is both a Runnable (so the pool can run it) and a Future (so you can query it). For the Runnable overloads, the work returns the supplied result (or null). This adapter is itself a small Command-pattern artifact: it turns the request into a runnable-plus-result-holder. ## Choosing between them | Need | Use | |---|---| | Fire-and-forget, no result, want failures on uncaught handler | `execute` | | A return value | `submit(Callable)` | | Completion signal / cancellation / timeout | `submit` + `Future` | | Many tasks, wait for all/any | `invokeAll` / `invokeAny` | ## Key terms - **Future<T>**: a placeholder for a result that will exist later; methods block or poll for it. - **ExecutionException**: the wrapper thrown by Future.get() when the task threw; the real cause is in getCause(). - **FutureTask**: the JDK class that is both Runnable and Future, used to back submit(). - **UncaughtExceptionHandler**: the thread-level callback invoked when an exception escapes a thread's run().
- You submit 100 tasks and never touch the Futures. One throws. What happens?Nothing visible: submit captures the exception inside that task's Future, and since you never call get(), it is silently swallowed — a common source of hidden failures. Use execute, or inspect every Future, to make errors observable.
- What two checked exceptions can Future.get() throw and what do they mean?InterruptedException (the waiting thread was interrupted while blocked) and ExecutionException (the task itself threw; the real cause is in getCause()).
saying these in an interview costs you the question
- Believing execute can take a Callable (it cannot — no result channel)
- Assuming a submitted task's exception will be printed automatically (it is hidden in the Future)
- Saying Future.get() throws the original exception directly (it throws ExecutionException wrapping it)
- Thinking submit always blocks — it returns immediately; only Future.get() blocks