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

  1. Ensure any code that depends on WorkerThread state runs only inside tasks scheduled by the engine's own executor service.
  2. Do not share engine-internal APIs (WorkerThread.getOrThrow) with externally spawned threads; capture needed context before leaving the worker thread.
  3. 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

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


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)