junit-team/junit5 · error · PreconditionViolationException

Launcher session has already been closed

Error message

Launcher session has already been closed

What it means

Thrown by the ClosedLauncher singleton when registerLauncherDiscoveryListeners() is invoked on a Launcher whose LauncherSession has already been closed. After DefaultLauncherSession.close() swaps the delegate to ClosedLauncher.INSTANCE (line 87-88), every Launcher method routes to a stub that rejects the call. It is a PreconditionViolationException signalling use-after-close.

Source

Thrown at junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java:109

			interceptor.close();
		}
	}

	@Override
	public NamespacedHierarchicalStore<Namespace> getStore() {
		return store;
	}

	private static class ClosedLauncher implements Launcher {

		static final ClosedLauncher INSTANCE = new ClosedLauncher();

		private ClosedLauncher() {
		}

		@Override
		public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) {
			throw new PreconditionViolationException("Launcher session has already been closed");
		}

		@Override
		public void registerTestExecutionListeners(TestExecutionListener... listeners) {
			throw new PreconditionViolationException("Launcher session has already been closed");
		}

		@Override
		public TestPlan discover(LauncherDiscoveryRequest launcherDiscoveryRequest) {
			throw new PreconditionViolationException("Launcher session has already been closed");
		}

		@Override
		public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners) {
			throw new PreconditionViolationException("Launcher session has already been closed");
		}

		@Override

View on GitHub (pinned to 956246301e)

Solutions

  1. Register all LauncherDiscoveryListeners before closing the LauncherSession, ideally right after openSession().
  2. Do not cache the Launcher beyond the session lifetime; obtain a fresh Launcher from a new session for each run.
  3. If using try-with-resources, move registerLauncherDiscoveryListeners inside the try block.
  4. Audit lifecycle code to ensure close() is the last operation on the session.

Example fix

// before
try (var session = factory.openSession()) {
    Launcher launcher = session.getLauncher();
}
launcher.registerLauncherDiscoveryListeners(listener); // throws

// after
try (var session = factory.openSession()) {
    Launcher launcher = session.getLauncher();
    launcher.registerLauncherDiscoveryListeners(listener);
    // ... execute
}
Defensive patterns

Strategy: validation

Validate before calling

// Track session liveness before registering listeners
if (session.isClosed()) { // you must track this; LauncherSession has no isOpen() API,
    throw new IllegalStateException("session closed"); // so mirror close() in a wrapper
}
launcher.registerLauncherDiscoveryListeners(listener);

Try / catch

try {
    launcher.registerLauncherDiscoveryListeners(listener);
} catch (PreconditionViolationException e) {
    if (e.getMessage().contains("already been closed")) {
        // reopen a session and retry, or fail fast with context
    } else { throw e; }
}

Prevention

When it happens

Trigger: Calling launcher.registerLauncherDiscoveryListeners(...) after the LauncherSession that produced the Launcher has been closed (e.g. via try-with-resources on LauncherSessionFactory.openSession()). The DelegatingLauncher held from before close now points at ClosedLauncher.INSTANCE.

Common situations: Storing the Launcher in a field/static, closing the session (try-with-resources block ending), then registering listeners in a later test run. IDE test runners or build tools that reuse a Launcher across sessions. Refactoring that moved session.close() earlier than listener registration.

Related errors


AI-assisted analysis of junit-team/junit5@956246301e (2026-08-04). Data as JSON: /data/errors/ed95cd8f45ac5911.json. Report an issue: GitHub.