🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGException in thread "main" java.lang.InterruptedException: sleep interrupted at java.base/java.lang.Thread.sleep(Native Method) at com.devinhyderabad.worker.JobRunner.run(JobRunner.java:5) at com.devinhyderabad.worker.WorkerMain.main(WorkerMain.java:6) // Interrupted inside Object.wait()/join(): NO message — the bare form: Exception in thread "main" java.lang.InterruptedException at java.base/java.lang.Object.wait(Native Method) at java.base/java.lang.Object.wait(Object.java:338) at com.devinhyderabad.worker.QueueDrainer.drain(QueueDrainer.java:7)

⚡ Quick Fix Works 80% of the time

Either propagate InterruptedException upward or restore the flag before continuing — never swallow it.

try { Job job = queue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore: cancellation stays visible return; // exit the loop promptly }

🧠 Why this Happens

Tap to expand the deep technical explanation

Every thread carries one boolean interrupt STATUS FLAG — not a queue of signals. interrupt() sets it; blocking primitives poll it on entry and after wakeups, and crucially they CLEAR the flag while throwing InterruptedException. That clearing is the entire tragedy: a swallowed exception leaves no trace of the request anywhere, so outer layers can never observe that cancellation was asked for. The message asymmetry is incidental hotspot plumbing — the sleep path constructs its exception with "sleep interrupted", the wait/join paths pass none. Checked-ness is deliberate design friction: the compiler forces every handler to consciously choose between propagating, restoring, or genuinely consuming the stop request.

The HITEC City Parking Spot Analogy:

An interrupt is a tap on the shoulder asking a swimmer to leave the pool. Swallowing it erases the tap from history — the lifeguard believes everyone left willingly. Re-raising the flag is signing the visitor book on the way out so the next checkpoint knows the evacuation is still underway.

🔁 How to Reproduce Confirm this is your error

Set Thread.currentThread().interrupt() BEFORE calling Thread.sleep(60_000) or lock.wait() — both throw instantly (zero waiting): sleep carries the message "sleep interrupted"; wait throws bare. Captured verbatim on OpenJDK 17; messages identical on Temurin 25 though native frames rename there (Thread.sleepNanos0, Object.wait0).

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Restore the interrupt flag, then stop the work

👉 Use this whenever you must catch InterruptedException inside a loop or task body.

Blocking methods CLEAR the flag while throwing, so catching without re-interrupting ERASES the cancellation request — thread pools then leak stuck workers and shutdown hangs forever. The pattern is always: catch, Thread.currentThread().interrupt(), exit promptly.

@Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { process(queue.take()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // re-mark break; // leave cleanly } } }
Solution 2

Propagate instead of handling when the caller owns lifecycle

👉 Use this for library/service methods whose callers sit above scheduling logic.

Declaring throws InterruptedException keeps the cancellation decision where lifecycle policy lives (executors, frameworks). Converting to unchecked exceptions severs that contract and forces every layer to reinvent shutdown handling.

public Report generate(Input in) throws InterruptedException { // caller decides: retry, reschedule, or abandon var part1 = heavyStepOne(in); var part2 = heavyStepTwo(part1); return assemble(part2); }
Solution 3

Decode the two pasted shapes at a glance

👉 Use this to triage which blocking call was interrupted before reading any code.

Message-bearing "sleep interrupted" comes specifically from Thread.sleep; the bare message-less form comes from Object.wait(), join(), and most j.u.c conditions. Semantics are identical — the flag was set during blocking — only the throw site differs, and native frame names keep evolving across builds.

// Shape A (Thread.sleep): java.lang.InterruptedException: sleep interrupted at java.base/java.lang.Thread.sleep(Native Method) // Shape B (wait/join/j.u.c.): java.lang.InterruptedException at java.base/java.lang.Object.wait(Native Method)
Solution 4

Cooperate with Future.cancel(true) in pooled tasks

👉 Use this when tasks run inside executor services and users hit cancel or shutdown-now.

cancel(true) sets the worker's flag; tasks honor it either by getting InterruptedException from blocking calls or by polling Thread.currentThread().isInterrupted() between CPU-bound chunks. Ignoring either channel makes shutdown-now indistinguishable from a hang.

Future<Report> f = pool.submit(this::expensiveReport); ... f.cancel(true); // sets flag -> blocking calls throw, loops can poll // Inside expensiveReport: if (Thread.currentThread().isInterrupted()) throw new CancellationException();
Solution 5

Swallow-spotting in reviews and thread dumps

👉 Use this to hunt the anti-pattern across an existing codebase or a hung service.

Empty catch blocks around InterruptedException silently kill cancellation; grep finds them instantly. On a running JVM, jstack revealing threads parked in TIMED_WAITING despite a pending shutdown request is the runtime symptom of exactly that swallow.

# Static sweep: grep -rn --include=*.java -A2 "catch (InterruptedException" src/ | less # Runtime confirmation: jstack <pid> | grep -B2 -A4 "TIMED_WAITING"

📋 Version Notes

Java 8

"Sleep interrupted" wording already present for Thread.sleep; bare form from wait/join; default stacks platform-dependent.

Java 11

Semantics unchanged; internal native method names begin diverging between builds.

Java 17

Captured verbatim here: Thread.sleep(Native Method) and Object.wait(Object.java:338) frame shapes.

Java 21

Virtual threads honor interruption identically (same restore rule!) — native frames renamed again on newest builds (sleepNanos0, wait0 observed on 25).

🛡️ How to Prevent This Next Time

Standardize a project snippet (catch -> restore -> exit), ban empty catch blocks via Checkstyle/ErrorProne rules, document every blocking call in public APIs, and smoke-test shutdown paths so cancellations complete within your SLA.