junit-team/junit5 · error · PreconditionViolationException

thread mode must not be INFERRED

Error message

thread mode must not be INFERRED

What it means

Thrown when TimeoutInvocationFactory.create() receives ThreadMode.INFERRED. INFERRED is a sentinel value that the JUnit engine resolves to either SAME_THREAD or SEPARATE_THREAD before reaching the factory — so passing INFERRED directly is an internal contract violation, not a user configuration error. This indicates a bug in the engine's internal dispatch logic or a custom extension that bypassed the resolution step.

Solutions

  1. Do not pass ThreadMode.INFERRED to create() — resolve it to SAME_THREAD or SEPARATE_THREAD first
  2. If you are writing a custom extension, use the engine's ThreadMode resolution logic before calling the factory
  3. Report as a JUnit bug if encountered through standard @Timeout annotations without custom extensions

Example fix

// before (incorrect direct call)
factory.create(ThreadMode.INFERRED, parameters);

// after (resolve to concrete mode first)
ThreadMode resolved = determineThreadMode(); // SAME_THREAD or SEPARATE_THREAD
factory.create(resolved, parameters);
Defensive patterns

Strategy: validation

Validate before calling

// Guard against passing INFERRED to the factory (internal use only)
import org.junit.jupiter.engine.extension.TimeoutInvocationFactory;
import org.junit.jupiter.api.Timeout.ThreadMode;

ThreadMode safeMode = (threadMode == ThreadMode.INFERRED) ? ThreadMode.SAME_THREAD : threadMode;
factory.create(safeMode, parameters);

Prevention

When it happens

Trigger: Calling TimeoutInvocationFactory.create(ThreadMode.INFERRED, parameters) directly. This should not be reachable through normal @Timeout usage because the engine resolves INFERRED upstream. A custom extension that programmatically invokes the factory with an unresolved ThreadMode could trigger it.

Common situations: Essentially unreachable in normal usage. Would only appear if a custom extension or engine modification incorrectly passes ThreadMode.INFERRED to the factory without resolving it first. Developers writing extensions that interact with the timeout machinery may encounter this during development.

Related errors


AI-assisted analysis of junit-team/junit5@f070c699a0 (2026-08-11). Data as JSON: /api/errors/02f4f27f6787759d. Report an issue: GitHub.

Appendix: source

Thrown at junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java:43

/**
 * @since 5.9
 */
class TimeoutInvocationFactory {

	private final Store store;

	TimeoutInvocationFactory(Store store) {
		this.store = Preconditions.notNull(store, "store must not be null");
	}

	<T extends @Nullable Object> Invocation<T> create(ThreadMode threadMode,
			TimeoutInvocationParameters<T> parameters) {
		Preconditions.notNull(parameters, "timeout invocation parameters must not be null");
		return switch (Preconditions.notNull(threadMode, "thread mode must not be null")) {
			case SAME_THREAD -> new SameThreadTimeoutInvocation<>(parameters,
				getThreadExecutorForSameThreadInvocation());
			case SEPARATE_THREAD -> new SeparateThreadTimeoutInvocation<>(parameters);
			case INFERRED -> throw new PreconditionViolationException("thread mode must not be INFERRED");
		};
	}

	@SuppressWarnings("resource")
	private ScheduledExecutorService getThreadExecutorForSameThreadInvocation() {
		return store.computeIfAbsent(SingleThreadExecutorResource.class).get();
	}

	@SuppressWarnings({ "deprecation", "try" })
	abstract static class ExecutorResource implements Store.CloseableResource, AutoCloseable {

		private final ScheduledExecutorService executor;

		ExecutorResource(ScheduledExecutorService executor) {
			this.executor = executor;
		}

		ScheduledExecutorService get() {

View on GitHub (pinned to f070c699a0)