What happens to a periodic task scheduled on a ScheduledExecutorService if its body throws an uncaught exception, and how do you handle it safely?
answer
- periodic + uncaught exception => future runs cancelled
- silent: captured in the Future, not the UncaughtExceptionHandler
- fix: try/catch the whole task body
- catch Exception not Throwable
- future.get() / afterExecute to surface it
basics
~20 sIf a periodic task throws an uncaught exception, that task is silently cancelled — it never runs again — and you usually see no log. The fix is to wrap the task body in try/catch so the exception never escapes, or inspect the returned ScheduledFuture.
solid answer
~50 sFor tasks submitted via scheduleAtFixedRate or scheduleWithFixedDelay, an uncaught exception (a Throwable escaping run()) suppresses all subsequent executions of that task — the periodic schedule is silently cancelled, the rest of the pool keeps working, and crucially nothing is printed to the console because the exception is captured in the task's ScheduledFuture rather than reaching the thread's UncaughtExceptionHandler. So a single transient failure can silently kill a recurring job for the lifetime of the process. The standard defense is to never let the exception escape: wrap the entire task body in try/catch, log it, and decide whether to continue. If you genuinely want to know about it, you can also call get() on the ScheduledFuture (which throws the wrapped cause once the task has terminated), or subclass ScheduledThreadPoolExecutor and override afterExecute/decorateTask to inspect failures. The try/catch-inside-the-task approach is the most common and robust, because it keeps the schedule alive and gives you control over retry/back-off.
go deeper
Knows that if a repeating task throws, it can stop running, and that wrapping the body in try/catch prevents it.
States the documented rule (subsequent executions suppressed) and applies the try/catch-the-body fix with logging.
Explains why it is silent (captured in the Future, not the UncaughtExceptionHandler), contrasts execute vs submit/schedule, and chooses catch Exception over Throwable with retry/back-off.
Builds a reusable safe-task wrapper, adds metrics/alerting on periodic-task death, and weighs afterExecute/decorateTask vs. body-guarding across a service fleet, treating silent cancellation as an SLO risk.
## The trap This is one of the most-asked gotchas about `ScheduledExecutorService`. You schedule a recurring job: ```java exec.scheduleAtFixedRate(this::poll, 0, 10, TimeUnit.SECONDS); ``` Weeks later it has silently stopped running, with **no error in the logs**. Why? ## What 'uncaught exception' means here A task is a `Runnable`; its `run()` method must not declare checked exceptions, but it can throw **unchecked** exceptions (`RuntimeException`, `Error`) at runtime — e.g., an NPE, an `IllegalStateException`, an arithmetic error. If your `run()` lets one of these propagate out (you did not catch it), it is an **uncaught exception** from the task's point of view. ## The documented behavior The Javadoc for `scheduleAtFixedRate`/`scheduleWithFixedDelay` states: *if any execution of the task encounters an exception, subsequent executions are suppressed.* In other words the periodic task is **cancelled** — it will never run again. The thread pool itself is fine; other tasks continue; only this one recurring task dies. ## Why it is silent With a normal `thread.start()`, an uncaught exception reaches the thread's `UncaughtExceptionHandler`, which by default prints a stack trace to `System.err`. But a scheduled (or any `submit`ted) task runs inside the pool and its outcome is captured into the task's **`Future`** instead. The exception becomes the Future's stored cause; it is only surfaced if someone calls `future.get()`. Since most code ignores the returned `ScheduledFuture`, the exception is swallowed — no stack trace, no log line. That is the dangerous part: the symptom is 'the job stopped' with zero diagnostics. (Note: `execute(Runnable)` — as opposed to `submit`/`schedule` — *does* route uncaught exceptions to the handler, but the scheduling methods use the Future path.) ## The fix — guard the task body The robust, idiomatic fix is to make sure nothing ever escapes `run()`: ```java exec.scheduleAtFixedRate(() -> { try { poll(); // real work } catch (Exception e) { // catch Exception, not Throwable, normally log.error("poll failed; continuing schedule", e); // optionally: back-off, increment a metric, etc. } }, 0, 10, TimeUnit.SECONDS); ``` Because no exception leaves the body, the schedule survives the failure and keeps firing. You also get a log line and the chance to apply retry/back-off policy. Catch `Exception` (recoverable) rather than `Throwable` — you usually do **not** want to swallow `Error`s like `OutOfMemoryError`. ## Alternative detection mechanisms - **Inspect the Future:** keep the returned `ScheduledFuture` and, when it is done, call `get()` — it throws an `ExecutionException` wrapping the original cause. Useful for one-shot `schedule()` results; awkward for periodic tasks (a healthy periodic Future never 'completes'). - **Override `afterExecute(Runnable, Throwable)`** on a `ScheduledThreadPoolExecutor` subclass to centrally log task failures. But for tasks wrapped in a `Future`, the `Throwable` argument is often `null` and you must unwrap the `Future` yourself, so this is subtle. - **`decorateTask`** lets you wrap each scheduled task to add logging. ## Operational takeaway Treat every periodic scheduled task as if it *will* throw eventually. Always wrap the body in try/catch (or a small helper that does), log failures, and emit a metric so you notice silent death. This single habit prevents the classic 'our cron-like job mysteriously stopped' incident.
- Why is the exception not printed to the console by default?Scheduled and submitted tasks are wrapped in a Future; the exception is stored as the Future's cause and only surfaces when someone calls get(). It does not reach the thread's UncaughtExceptionHandler, so the default stack-trace print never happens — hence the silence.
- Should you catch Exception or Throwable in the task body?Normally catch Exception. Catching Throwable would also swallow Errors such as OutOfMemoryError or StackOverflowError, which you generally must not suppress — they signal the JVM is in trouble and the task (or process) should not pretend to continue normally.
saying these in an interview costs you the question
- Believing the exception prints a stack trace by default (it is swallowed into the Future).
- Thinking one failing task crashes the whole pool or the JVM (only that periodic task is cancelled).
- Assuming the schedule resumes on the next tick after an exception (it never runs again).
- Catching Throwable and swallowing Errors like OutOfMemoryError.
- Confusing execute() (routes to UncaughtExceptionHandler) with schedule()/submit() (captures in the Future).