What are the built-in RejectedExecutionHandler policies, when does rejection occur, and how would you choose between them?
answer
- reject = queue full AND at max (or shut down)
- Abort = throw (default)
- CallerRuns = backpressure, no loss
- Discard = silent drop; DiscardOldest = drop head
- custom handler = metric + log + fallback
basics
~20 sRejection happens when the queue is full and the pool is already at its maximum size (or after shutdown). The built-in policies are: throw an exception (AbortPolicy, the default), run the task on the calling thread (CallerRunsPolicy), silently drop it (DiscardPolicy), or drop the oldest queued task (DiscardOldestPolicy).
solid answer
~50 sA task is rejected when execute() can neither enqueue it (queue full) nor add a worker (pool at maximumPoolSize), or when the executor is shutting down. The RejectedExecutionHandler decides what to do. The four built-ins are: AbortPolicy (default) throws RejectedExecutionException so the caller learns work was refused; CallerRunsPolicy runs the task in the submitting thread, which throttles producers and gives natural backpressure without losing work; DiscardPolicy silently drops the new task; DiscardOldestPolicy drops the head of the queue and retries the new one. Choose AbortPolicy when callers can handle/retry failures, CallerRunsPolicy when you want backpressure and must not lose tasks, and the Discard variants only when dropping work is genuinely acceptable (e.g. stale telemetry). For anything important, a custom handler that records a metric, logs, and applies a bounded retry or fallback is usually better than the silent discards.
code
java · 12 lines// Custom handler: observe overload instead of failing silently
RejectedExecutionHandler handler = (task, exec) -> {
Metrics.counter("pool.rejected").increment();
if (exec.isShutdown()) {
throw new RejectedExecutionException("pool shut down");
}
// backpressure: run on caller, throttling the producer
task.run();
};
new ThreadPoolExecutor(8, 32, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500), handler);go deeper
Knows rejection can happen under overload and that there's a default that throws an exception.
Lists the four built-in policies and the exact condition (queue full and at max, or shut down) that triggers rejection.
Picks a policy per use case, explains CallerRunsPolicy backpressure, and prefers a custom handler that records metrics over silent discards.
Designs the rejection path as part of system resilience: observability, load shedding, durable overflow, and how it interacts with upstream retries and autoscaling.
## When does rejection happen? Recall the submission order: fill core → enqueue → grow to max → **reject**. A `ThreadPoolExecutor` rejects a task in exactly two situations: 1. The **work queue is full** *and* the pool is already at **`maximumPoolSize`**, so it can neither buffer the task nor create another worker. 2. The executor has been **shut down** (`shutdown()`/`shutdownNow()` called), after which it refuses new work. When this occurs, the executor calls `handler.rejectedExecution(task, executor)` on its configured `RejectedExecutionHandler`. Rejection is the pool's **backpressure signal** — the moment the system says 'I'm at capacity.' What you do with that signal is a design decision. ## The four built-in policies (nested classes of `ThreadPoolExecutor`) - **`AbortPolicy` (the default).** Throws `RejectedExecutionException` (an unchecked exception) from the submitting call. The producer immediately learns the task was refused and can decide to retry, back off, or fail. Good when callers are prepared to handle the exception. Dangerous if no one catches it — it can crash a request thread or bubble up unexpectedly. - **`CallerRunsPolicy`.** The thread that called `execute()` runs the rejected task **itself**, synchronously. The task is *not* lost, and because the producer is now busy executing, it can't submit more work for a while — this naturally **slows the producer down to the pool's rate** (backpressure). Excellent default for pipelines where you must not drop work and want self-throttling. Caveat: it ties up the caller (e.g. a request-handling thread or an event loop), so it's wrong where the caller must stay responsive. - **`DiscardPolicy`.** Silently **drops** the new task — no exception, no log. Only acceptable when losing work is genuinely fine (e.g. best-effort metrics). Its silence is also its danger: lost work is invisible. - **`DiscardOldestPolicy`.** Drops the **oldest** task waiting in the queue (the head) and retries the new one. Implies 'newest data is most valuable' — appropriate for things like live dashboards where stale updates are worthless. Don't use it with a `PriorityBlockingQueue` (it discards the highest-priority head) or where ordering matters. ## Custom handlers Real systems often implement `RejectedExecutionHandler` directly to: increment a 'rejected tasks' metric, log at WARN, write the task to a durable buffer / dead-letter store, apply a bounded blocking `put()` (turning the bounded queue into a blocking submit), or trigger autoscaling. A common pattern is a metric-recording wrapper around `CallerRunsPolicy`. ## Choosing | Want… | Use | |---|---| | Caller to be notified and handle failure | `AbortPolicy` | | No loss + automatic producer throttling | `CallerRunsPolicy` | | Best-effort, droppable, newest matters | `DiscardOldestPolicy` | | Best-effort, droppable, order irrelevant | `DiscardPolicy` | | Observability / durability / custom backpressure | custom handler | The key insight: rejection is **not an error to be suppressed** but a capacity signal to be handled deliberately. Silent discards hide overload; choose them only with eyes open.
- How does CallerRunsPolicy create backpressure?The submitting thread executes the rejected task itself, so while it's busy running that task it can't submit new ones. This throttles the producer down to the pool's drain rate without losing any work.
- What is the default RejectedExecutionHandler if you don't specify one?AbortPolicy, which throws a RejectedExecutionException (unchecked) on the submitting call when the task can't be accepted.
A busy ER at capacity: AbortPolicy turns the patient away with a clear 'we're full' notice; CallerRunsPolicy makes the person who brought them treat them on the spot (slowing further arrivals); DiscardPolicy quietly ignores them; DiscardOldestPolicy bumps whoever's been waiting longest to make room for the newest.
saying these in an interview costs you the question
- Thinking rejection happens as soon as all core threads are busy (it only happens at queue-full AND max, or after shutdown).
- Choosing DiscardPolicy by default and hiding overload (silent data loss).
- Using CallerRunsPolicy on a latency-critical caller (e.g. an HTTP request thread) without realizing it blocks that thread.
- Assuming AbortPolicy's exception is checked or always caught — uncaught RejectedExecutionException can break the caller.