junit-team/junit5 · error · IllegalStateException
Not on a worker thread
Error message
Not on a worker thread
What it means
WorkerThread.getOrThrow() throws IllegalStateException when the current thread is not a WorkerThread (the executor's owned thread type). WorkerThreadPoolHierarchicalTestExecutorService associates per-thread state (leases, queue processing) only with its WorkerThread instances, so calling code that needs that state from any other thread is a programming error.
Solutions
- Ensure any code that depends on WorkerThread state runs only inside tasks scheduled by the engine's own executor service.
- Do not share engine-internal APIs (WorkerThread.getOrThrow) with externally spawned threads; capture needed context before leaving the worker thread.
- If authoring a custom executor service, only call WorkerThread.getOrThrow() from threads you created as WorkerThread instances.
Example fix
// before - runs on an external executor otherExecutor.submit(() -> doWorkRequiringWorkerThread()); // after - run on the engine worker, capture context beforehand var captured = contextSnapshot; worker.submit(() -> doWorkWith(captured));
Defensive patterns
Strategy: validation
Validate before calling
// Before calling worker-thread-dependent code, confirm the current thread
if (!(Thread.currentThread() instanceof WorkerThread)) {
throw new IllegalStateException("must run on a worker thread");
} Type guard
static boolean isWorkerThread() {
return Thread.currentThread().getClass().getName().endsWith("WorkerThread")
|| Thread.currentThread() instanceof org.junit.platform.engine.support.hierarchical.WorkerThreadPoolHierarchicalTestExecutorService.WorkerThread;
} Try / catch
try {
WorkerThread.getOrThrow();
} catch (IllegalStateException e) {
// reschedule the work onto the engine's own executor
} Prevention
- Only touch engine-internal thread state from tasks scheduled by the engine executor.
- Capture needed context before dispatching to external executors.
- Keep test-spawned threads away from engine internals.
When it happens
Trigger: Invoking WorkerThread.getOrThrow() (or internal code paths that call it) from a non-worker thread: a foreign ExecutorService thread, the main thread, a CompletableFuture default pool, or a thread the test itself spawned. This happens when test/extension code schedules work onto an external executor that then calls back into engine-internal APIs.
Common situations: A test or extension spawns its own threads / uses a shared ForkJoinPool.commonPool() and those threads touch engine internals expecting WorkerThread state; mixed executor services within one engine; a custom HierarchicalTestExecutorService whose tasks dispatch to an unrelated pool.
Related errors
- Failed to create ForkJoinPool
- Scheduled executor could not be stopped in an orderly manner
- Unsupported AnnotatedElement type for @TempDir
AI-assisted analysis of junit-team/junit5@f070c699a0 (2026-08-11).
Data as JSON: /api/errors/ed6bf6e21579fff1.
Report an issue: GitHub.
Appendix: source
Thrown at junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/WorkerThreadPoolHierarchicalTestExecutorService.java:277
@Nullable
WorkerLease workerLease;
WorkerThread(Runnable runnable, String name) {
super(runnable, name);
}
static @Nullable WorkerThread get() {
if (Thread.currentThread() instanceof WorkerThread workerThread) {
return workerThread;
}
return null;
}
static WorkerThread getOrThrow() {
var workerThread = get();
if (workerThread == null) {
throw new IllegalStateException("Not on a worker thread");
}
return workerThread;
}
WorkerThreadPoolHierarchicalTestExecutorService executor() {
return WorkerThreadPoolHierarchicalTestExecutorService.this;
}
void processQueueEntries(WorkerLease workerLease, BooleanSupplier doneCondition) {
this.workerLease = workerLease;
while (!executor.isShutdown()) {
if (doneCondition.getAsBoolean()) {
logger.trace(() -> "yielding resource lock");
break;
}
if (workQueue.isEmpty()) {
logger.trace(() -> "no queue entries available");
break;View on GitHub (pinned to f070c699a0)