execute(Runnable) returns nothing and an uncaught exception propagates to the thread's UncaughtExceptionHandler. submit(...) returns a Future and any exception is captured inside that Future — it only surfaces when you call get(). This difference is a classic source of silently swallowed errors.
The exception trap
ExecutorService pool = Executors.newFixedThreadPool(2);
// execute: exception escapes to the thread; handler/stderr sees a stack trace
pool.execute(() -> { throw new RuntimeException("boom-execute"); });
// submit: exception is swallowed into the Future, never printed
Future<?> f = pool.submit(() -> { throw new RuntimeException("boom-submit"); });
// ... and unless someone does f.get(), no one ever knows it failed
f.get(); // NOW it throws ExecutionException wrapping the RuntimeException
With execute, the throwable bubbles out of Worker.run(), the worker thread dies, and the pool's UncaughtExceptionHandler (default: print to stderr) fires. With submit, ThreadPoolExecutor wraps your task in a FutureTask whose run() catches the throwable and stores it. The worker survives; the error sits dormant in the Future. If you never call get(), the failure vanishes — no log, no alert.
API differences
execute(Runnable)— fromExecutor. Returnsvoid. Cannot accept aCallable.submit(Runnable)/submit(Callable<T>)— fromExecutorService. ReturnsFuture<T>for the result and exception, plus cancellation.
How to avoid the swallowed exception
Always handle the result of a submit, or catch inside the task:
Future<?> f = pool.submit(task);
// later, when collecting:
try { f.get(); }
catch (ExecutionException e) { log.error("task failed", e.getCause()); }