PRACTICAL GUIDE / AutoCloseable Selenium session Java

Make Selenium sessions close even when Java tests fail

Build a Java session wrapper that always calls quit, preserves the original test failure, and exposes cleanup leaks before they exhaust Selenium Grid.

By The Testing AcademyUpdated August 7, 202621 min read
All field guides
In this guide8 sections
  1. How a session escapes its test
  2. Build a resource boundary that owns quit
  3. Prove cleanup without hiding the test failure
  4. Make suppressed quit failures visible to the reporter
  5. Distinguish a leaked session from near misses
  6. When merged timestamps impersonate a leaked session
  7. Roll the change into an existing suite
  8. Split ownership without splitting the evidence
  9. Accept the cost, and know when not to use it

What you will learn

  • How a session escapes its test
  • Build a resource boundary that owns quit
  • Prove cleanup without hiding the test failure
  • Make suppressed quit failures visible to the reporter

A checkout test fails on its first assertion, and the Chrome window is still present twenty minutes later. After enough failures, Grid has no free slots even though the test run has finished. The assertion is not the leak. The session was created in one scope and left for a teardown hook in another.

How a session escapes its test

WebDriver has two cleanup calls that are easy to confuse. close asks the remote end to close the current top-level browsing context. If the session owns another window, the session can continue. quit asks Selenium to end the session and close every associated window. A framework that wants to release local driver processes, remote browser slots, profiles, and Grid bookkeeping must make quit the terminal operation.

The common JUnit shape hides the ownership problem:

Java
class CheckoutTest {
    private WebDriver driver;

    @BeforeEach
    void startBrowser() throws MalformedURLException {
        driver = new RemoteWebDriver(
            new URL(System.getenv("GRID_URL")),
            new ChromeOptions()
        );
    }

    @Test
    void rejectsAnExpiredCard() {
        driver.get("https://shop.example.test/checkout");
        driver.findElement(By.id("card-number")).sendKeys("4000000000000069");
        driver.findElement(By.id("pay")).click();
        assertEquals(
            "Card expired",
            driver.findElement(By.id("payment-error")).getText()
        );
    }

    @AfterEach
    void stopBrowser() {
        if (driver != null) {
            driver.quit();
        }
    }
}

This code looks responsible, and JUnit normally calls AfterEach even when the assertion fails. The weak point appears when real framework code grows around it. A base class creates the driver, an extension captures a screenshot, another extension publishes a video link, and the base class eventually calls quit. If screenshot capture throws, or an extension aborts the remaining callback chain incorrectly, cleanup may never run. If a test helper creates a second driver in a local variable, the field-based teardown does not know it exists. If a parameterized invocation overwrites the field before the previous invocation finishes, the wrong session gets closed.

A process exit can also bypass Java cleanup entirely. SIGKILL, an out-of-memory termination, a lost CI worker, and a machine restart do not execute finally blocks or close methods. AutoCloseable cannot solve those cases. Grid-side session timeouts and disposable workers remain necessary safety nets. The value of a resource scope is narrower and still important: every normal return, assertion failure, unchecked exception, and checked exception follows the same ownership path.

Try-with-resources gives that path a language-level boundary. Java closes initialized resources when execution leaves the try block. Resources close in reverse order, which matters if a test owns both a browser and another dependent resource. When the body throws and close also throws, Java preserves the body exception and records the close exception as suppressed. That behavior is much safer than a broad finally block that replaces the product failure with a cleanup failure.

One more boundary deserves attention. A constructor can fail before the wrapper exists. If RemoteWebDriver cannot create a session, there is nothing to close locally. If the remote browser was created but the response was lost on the network, the client may never receive a session ID. That is a Grid or provider orphan, not a Java scope leak. Diagnose it from server-side new-session events and enforce a remote idle timeout.

Build a resource boundary that owns quit

WebDriver itself is not AutoCloseable. Do not cast it, and do not subclass RemoteWebDriver merely to add a close method. A small composition wrapper makes ownership visible without changing Selenium's type hierarchy.

Java
package example.selenium;

import java.net.URL;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.SessionId;

public final class SeleniumSession implements AutoCloseable {
    private final RemoteWebDriver driver;
    private final AtomicBoolean closed = new AtomicBoolean();

    private SeleniumSession(RemoteWebDriver driver) {
        this.driver = Objects.requireNonNull(driver);
    }

    public static SeleniumSession start(URL gridUrl, Capabilities capabilities) {
        return new SeleniumSession(
            new RemoteWebDriver(gridUrl, capabilities)
        );
    }

    public WebDriver driver() {
        ensureOpen();
        return driver;
    }

    public String sessionId() {
        ensureOpen();
        SessionId id = driver.getSessionId();
        if (id == null) {
            throw new IllegalStateException("Remote session has no session ID");
        }
        return id.toString();
    }

    @Override
    public void close() {
        if (closed.compareAndSet(false, true)) {
            driver.quit();
        }
    }

    private void ensureOpen() {
        if (closed.get()) {
            throw new IllegalStateException("Selenium session is already closed");
        }
    }
}

The AtomicBoolean is not an invitation to use one driver from several threads. It only makes duplicate cleanup harmless when an extension and the test scope both attempt to close the same owner during a migration. The WebDriver commands remain confined to the test thread.

Keep the wrapper deliberately small. Returning WebDriver prevents most tests from depending on RemoteWebDriver methods. The sessionId method exposes the one piece of remote identity that diagnostics need. Avoid forwarding every WebDriver method through the wrapper. A forwarding layer turns into a second Selenium API, and it becomes difficult to upgrade.

A test now states exactly where the browser stops being valid:

Java
@Test
void expiredCardShowsAUsefulMessage() throws Exception {
    URL gridUrl = new URL(System.getenv("GRID_URL"));
    ChromeOptions options = new ChromeOptions();
    options.setCapability("se:name", "expiredCardShowsAUsefulMessage");

    try (SeleniumSession session = SeleniumSession.start(gridUrl, options)) {
        WebDriver driver = session.driver();
        System.out.printf(
            "phase=session-start test=expired-card session=%s%n",
            session.sessionId()
        );

        driver.get("https://shop.example.test/checkout");
        driver.findElement(By.id("card-number"))
            .sendKeys("4000000000000069");
        driver.findElement(By.id("pay")).click();

        assertEquals(
            "Card expired",
            driver.findElement(By.id("payment-error")).getText()
        );
    }
}

The capability se:name is useful display metadata in the Grid UI. It is not the session identity and must not replace it in logs. Several retries can share the same test name, but every successful new-session response has its own session ID.

The scope should normally cover one test attempt, not an entire class. That choice prevents cookies, open windows, storage, and browser state from leaking into the next test. It also means a retry creates a fresh wrapper and a fresh session. The cost is session startup time. On a busy remote provider, creating a browser for every attempt may add seconds or queue time. That is a real performance cost, but it buys isolation and makes ownership easy to audit.

Sometimes a test needs two browsers, such as a customer and an administrator. Create two named resources in the same try declaration. Java closes the second one first. Make that ordering intentional. If the administrator session needs to observe the customer disconnect, declare the customer second so it closes first. Otherwise use nested scopes and make the interaction explicit.

Never let a page object own the wrapper. Page objects can use a WebDriver, but they should not decide when the session ends. The code that creates the resource must remain the code that defines its lifetime. This single rule prevents most hidden-driver leaks.

Prove cleanup without hiding the test failure

A green assertion does not prove quit ran. The useful evidence has three parts: a start record containing the session ID, a scope-exit record, and a Grid event showing deletion of that same session. Log the ID before any navigation so a failure on the first command still has an identity.

Do not put a successful cleanup message after driver.quit inside close unless you also record failures. A network error can make quit throw before that message. A better integration point wraps the resource scope:

Java
static void runWithBrowser(
        String testId,
        URL gridUrl,
        ChromeOptions options,
        ThrowingConsumer<WebDriver> testBody) throws Exception {

    SeleniumSession session = SeleniumSession.start(gridUrl, options);
    String sessionId = session.sessionId();
    Throwable testFailure = null;

    try (session) {
        System.out.printf(
            "event=session-start test=%s session=%s%n",
            testId,
            sessionId
        );
        testBody.accept(session.driver());
    } catch (Throwable failure) {
        testFailure = failure;
        throw failure;
    } finally {
        System.out.printf(
            "event=session-scope-exit test=%s session=%s outcome=%s%n",
            testId,
            sessionId,
            testFailure == null ? "body-passed" : "body-failed"
        );
    }
}

@FunctionalInterface
interface ThrowingConsumer<T> {
    void accept(T value) throws Exception;
}

The scope-exit line says control left the block. It does not claim the remote session ended. If quit throws while a test exception is already in flight, inspect failure.getSuppressed() in the reporter. Some reporting systems print only the primary stack trace, which makes a slot leak look unrelated to the test. Add a contract test for the reporter before relying on suppressed exceptions operationally.

A CI job with its own isolated Grid can use Grid's GraphQL endpoint as a coarse leak gate. It should not run against a shared environment because other jobs may own legitimate sessions.

YAML
- name: Run Selenium tests
  env:
    GRID_URL: http://127.0.0.1:4444
  run: mvn --batch-mode test

- name: Check isolated Grid for active sessions
  if: always()
  run: |
    response=$(curl --fail --silent --show-error       -H 'Content-Type: application/json'       --data '{"query":"{ grid { sessionCount } }"}'       http://127.0.0.1:4444/graphql)
    active=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["grid"]["sessionCount"])' <<< "$response")
    if [ "$active" -ne 0 ]; then
      echo "Selenium leak check failed: $active session(s) remain"
      exit 1
    fi

This gate catches a remaining session, but it does not tell you which test leaked it. Keep per-test session IDs in the test log. For an active session, Grid's GraphQL session query can return its node ID, node URI, capabilities, and start time. That lets an engineer move from the leaked slot to the responsible test attempt.

A second contract test should force the body to fail. Run a test that creates a session, stores its ID, throws a deliberate AssertionError, and then checks that Grid no longer reports the session. Do not make that test part of every application suite if provider billing or startup latency is high. Run it when the lifecycle library changes and in a small nightly infrastructure suite.

The expected failure record should look like this:

Shell
event=session-start test=cleanup-contract session=8bc52f4d0f4f6d71
event=session-scope-exit test=cleanup-contract session=8bc52f4d0f4f6d71 outcome=body-failed
org.opentest4j.AssertionFailedError: deliberate lifecycle check

If the Grid log contains a DELETE event for session 8bc52f4d0f4f6d71, the resource boundary worked. If it contains a deletion attempt plus a connection error, cleanup was attempted but not confirmed. If the test log has no scope-exit line, the JVM likely stopped or the process was killed. Those outcomes need different fixes.

Make suppressed quit failures visible to the reporter

The forced assertion check covers the usual path, but it does not prove that the reporting stack retains a second failure from quit. Test this behavior without breaking a real Grid connection. The following small program substitutes a deterministic WebDriverException for a failed delete-session request, then prints the throwable exactly as an ordinary Java reporter should receive it.

Java
import org.openqa.selenium.WebDriverException;

public final class SuppressedFailureContract {
    public static void main(String[] args) {
        try {
            runFailingScope();
        } catch (Throwable failure) {
            System.err.printf(
                "primary=%s suppressed=%d%n",
                failure.getClass().getName(),
                failure.getSuppressed().length
            );
            failure.printStackTrace(System.err);
        }
    }

    private static void runFailingScope() throws Exception {
        try (AutoCloseable session = () -> {
            throw new WebDriverException(
                "simulated delete-session failure"
            );
        }) {
            throw new AssertionError("simulated checkout assertion");
        }
    }
}

The stable part of the output has this shape. Selenium also appends binding and system details to WebDriverException output, so the remaining lines depend on the installed version and host.

Example
primary=java.lang.AssertionError suppressed=1
java.lang.AssertionError: simulated checkout assertion
    at SuppressedFailureContract.runFailingScope(...)
    at SuppressedFailureContract.main(...)
    Suppressed: org.openqa.selenium.WebDriverException: simulated delete-session failure
        at SuppressedFailureContract.lambda$runFailingScope$0(...)
        ...

The AssertionError and the WebDriverException are sibling failures from the body and resource closure. The cleanup error is not the cause of the assertion, so looking only at getCause() will miss it. Java exposes it through getSuppressed(), and printStackTrace includes it under the primary exception. A result adapter that serializes only class, message, and cause silently discards the evidence.

Run the complementary case too. Remove the AssertionError and let the body return normally. The WebDriverException from close then becomes the primary failure, with no suppressed entry. A reporter must fail the test in both cases. If it marks the first case as an ordinary assertion failure and the second as an infrastructure failure, that classification is reasonable, but both throwable trees must remain available.

With a real RemoteWebDriver, do not depend on one exact quit exception class or message. A local connection failure, a remote error response, and an already unreachable browser can produce different WebDriverException subclasses and diagnostic text. The durable questions are whether quit was attempted, whether the primary throwable contains a suppressed cleanup failure, and whether Grid still lists the captured session ID.

This contract often breaks first at a custom JUnit listener, CI test-result converter, or dashboard ingestion step. Inspect the raw test process log before blaming try-with-resources. If the raw stack contains Suppressed: but the dashboard does not, the resource boundary preserved both failures and the reporting path lost information. Preserve the full exception tree, then redact credentials or sensitive capability values at the logging boundary. Keep the session ID, because removing it makes the cleanup failure impossible to correlate with Grid.

Distinguish a leaked session from near misses

A busy Grid and a leaked Grid look alike from the test runner. Both produce new-session timeouts. Start with session inventory, not the timeout message. If GraphQL shows active sessions whose tests already ended, investigate ownership. If it shows no active sessions but a nonzero queue, capacity or capability matching is the stronger suspect.

The first near miss is close versus quit. A test closes its only visible window, then a later quit call fails with NoSuchSessionException. Depending on browser behavior, the final window close may already end the session. That is not evidence that close is a reliable replacement. Search the Grid record. A clean deletion after the window close means the session ended; an active session with a second hidden or popup window means it did not.

The second near miss is a slow quit. Remote providers may take time to upload video, console logs, or other artifacts after the delete-session request. The client scope can finish before a dashboard marks the job complete. Do not label that delay a leak solely from the provider UI. Look for the WebDriver delete response and the provider's terminal state. Adding an arbitrary sleep makes every test slower and still races on a bad day.

The third near miss is a browser process that remains after Grid removed the session. This is a node cleanup problem. The test performed its protocol-level duty, but chromedriver, geckodriver, or the browser process did not exit. Compare Grid session inventory with operating-system processes on the node. Fix node recycling, driver versions, or container teardown rather than adding a second quit call in Java.

The fourth near miss occurs before a session ID exists. A new-session HTTP request times out, yet the node later launches a browser. The wrapper cannot call quit because RemoteWebDriver construction never returned. Match the server-side new-session trace to node activity, reduce uncertain network handoffs, and configure the remote service to reap idle sessions. Catching the constructor exception and retrying immediately can double the orphan count.

A fifth case is a dead test worker. No Java technique runs after SIGKILL. An isolated browser container per session limits the damage. Grid idle timeouts, provider session limits, and CI worker cleanup provide the backstop. Treat the wrapper as the first line of defense, not the only one.

Finally, distinguish a true cleanup exception from a reporter error. Screenshot capture in a failure callback can throw NoSuchWindowException after the test already closed a window. If the callback prevents later callbacks, the framework integration is broken. Capture diagnostics before resource closure, make evidence collection best-effort, and ensure an attachment failure cannot veto quit.

When merged timestamps impersonate a leaked session

Clock and ingestion order can produce almost the same incident timeline as a failed quit. The test process emits scope exit, an inventory snapshot appears to show the session still active, and a deletion record is displayed later. A reviewer concludes that cleanup lagged or required an external reaper. The competing cause is that the snapshot was taken before deletion on the Grid host but sorted after scope exit by an unsynchronized wall clock, or delivered late by the log pipeline.

Separate the cases inside each evidence source before merging them. A healthy client trace reaches scope exit for the captured session ID. A healthy Grid trace receives deletion for that same ID and no later Grid-local inventory sample contains it. Those two local orders can be valid even when their wall-clock strings overlap or appear reversed. A broken trace has no deletion for the ID, or has a later Grid-local inventory sample that still contains it after deletion failed. The exact ID and each source's own record order are decisive.

The misleading field is the cross-host timestamp. Millisecond precision does not imply synchronized clocks, and an ingestion timestamp describes arrival at the logging system rather than occurrence at the source. Keep a monotonically increasing sequence in the test-process lifecycle log, preserve the native ordering of Grid events, and attach one wall-clock anchor to each source for approximate joining. Do not invent a single total order by sorting every line from every machine on its displayed time.

Diagnostic output should therefore show source, local sequence or source order, session ID, event, and wall-clock time as separate fields. In a healthy example, the client sequence advances from start to scope exit, while the Grid's own order advances from session creation to deletion. In a broken example, client scope exit is present but Grid's stream ends with the same ID active and no deletion. In a misleading example, all healthy transitions exist but the combined viewer places the inventory snapshot after deletion. Correct the timeline or logging pipeline in that last case. Changing the Java close path cannot repair reordered evidence.

Roll the change into an existing suite

Start by locating every place that constructs ChromeDriver, FirefoxDriver, EdgeDriver, or RemoteWebDriver. Factories count too. The migration is incomplete while any helper can create an unowned driver. Do not begin by changing teardown hooks. First introduce the wrapper and make new tests use it.

Land reporter support before making the wrapper the default. The first break in many established suites is not browser creation. It is a failure adapter that assumes one throwable and drops suppressed cleanup errors, or a screenshot extension that runs after the resource has closed. Make the adapter retain the complete throwable tree, then place evidence collection inside the live-session scope. Only after those contracts pass should the central factory return an owner rather than a bare driver. That order keeps a lifecycle improvement from making the original test failure harder to see.

Next, add session-start logging to the current factory. That gives you a baseline of created IDs before lifecycle behavior changes. Compare the number of unique session starts with confirmed delete events in an isolated run. Existing leaks become visible, and the team can tell whether the wrapper improves the ratio.

Move one test package at a time. For each package, place the wrapper in the narrowest scope that matches current semantics. Most tests should use a method scope. Classes that intentionally reuse authentication may need a class scope temporarily. Mark those exceptions in code review so they do not become the default pattern.

During the transition, duplicate cleanup is likely. The AtomicBoolean prevents the wrapper from issuing quit twice, but an old AfterEach hook may still call quit on the underlying WebDriver. Change the hook to close the owner when one exists. Do not swallow NoSuchSessionException globally; it can reveal use-after-close in test code. Remove the old hook only after every test in that ownership group has migrated.

Exercise three cases before expanding the rollout: a passing test, an assertion failure, and a setup failure after session creation. The setup failure is often missed because the test body never starts. If session creation happens inside the resource initializer and later setup stays inside the try block, close still runs.

Retries need explicit attempt identity. A retried test must open a new resource and log a new session ID. If a retry extension reuses the failed driver's field, the browser may be in an unknown state and the first attempt's close may terminate the retry. Record test ID, attempt number, and session ID together.

Watch two metrics during rollout. Session creation count should roughly match confirmed session deletion count for completed attempts. New-session queue time may rise because method-scoped browsers create more sessions. If queue time becomes unacceptable, add capacity or reduce expensive UI coverage. Do not restore shared mutable browsers merely to hide the infrastructure cost.

After adoption, make the wrapper constructor private and keep the factory in one module. A public constructor that accepts any driver encourages tests to wrap already shared instances, which blurs ownership again. The resource should be born inside its owner whenever practical.

The rollout has a maintenance cost beyond browser startup. Centralizing creation means every new browser option or provider-specific capability must pass through the lifecycle module instead of being added inside one test. That review step slows unusual experiments and gives the module owners more compatibility work during Selenium or provider changes. It is still preferable to invisible ownership, but teams should budget for the factory as supported infrastructure rather than treating the wrapper as a finished utility class.

Split ownership without splitting the evidence

The test-framework team owns the Java boundary: the factory, the AutoCloseable wrapper, duplicate-close behavior, and the contract that the reporter retains suppressed exceptions. Individual suite owners control scope. They must identify tests that intentionally keep a browser beyond one method, remove helpers that create hidden sessions, and keep attempt identifiers accurate. The Grid or provider team owns server-side deletion evidence, session retention policy, and node cleanup after the remote session is gone. Giving all three responsibilities to whichever team sees the failed build first guarantees slow triage.

A handoff from a suite owner to the framework team should contain the test identifier, retry attempt, exact session ID, creation and scope-exit times, the primary throwable, every suppressed throwable, and the raw lifecycle lines surrounding close. A handoff to the Grid team should add the deletion request or its absence, the later inventory state for that same ID, and the node identity when the service exposes it. State whether the worker was cancelled or killed. Do not send only a screenshot of an occupied-slot counter, because it removes identity and timing, the two facts needed to choose an owner.

The receiving team should be able to answer one bounded question. If the Java trace never reaches scope exit during a normal process lifetime, the suite or framework path still owns the defect. If scope exit contains a quit failure and the remote service never receives deletion, the client transport boundary needs investigation. If deletion succeeds and Grid no longer lists the session but a browser process remains, node operations owns the cleanup defect. This division avoids asking a test author to repair a stale node or asking an infrastructure operator to infer Java callback order from a final CI status.

Accept the cost, and know when not to use it

A resource wrapper adds a type, a factory, lifecycle logs, and contract tests. One session per test also increases browser startup time and remote-provider usage. Diagnostics may retain session IDs and node details, which need the same access controls and retention policy as other infrastructure logs. These costs are concrete, but they are usually smaller than a Grid that slowly loses capacity during a failing run.

Do not use method-scoped AutoCloseable ownership when the browser intentionally belongs to a larger fixture. A suite that performs a two-hour manual-assisted certification in one long session needs a suite-level owner. Closing at every method would destroy the workflow. The same wrapper can still help, but its scope must match the real lifetime.

Avoid this pattern as a bandage for thread sharing. Making close atomic does not make navigation, window selection, element references, or cookies thread-safe. If concurrent tests share one driver, give each test a separate session or serialize the entire interaction. Cleanup correctness cannot repair command interleaving.

Do not place the wrapper in page objects, service clients, or assertion helpers. Those components borrow a driver. They do not own it. Hidden close calls create failures that look like random NoSuchSessionException errors several steps later.

This technique does not catch browser work that escapes into a background task. A test can submit an asynchronous command, leave the try block, and close the session correctly before that command runs. Grid shows no leak, yet the background task later fails against a deleted session or mutates state after the assertion phase. The ownership boundary guarantees cleanup when its thread exits. It does not prove that every borrowed driver user finished before exit. Track and join background work separately, or keep WebDriver calls on the owning test thread.

Skip a custom wrapper if your test framework already supplies a well-tested per-test driver resource with guaranteed quit semantics and observable cleanup. Adding a second owner makes the lifecycle worse. Verify the framework with the same forced-failure contract instead of assuming its hook ordering.

Do not treat a zero GraphQL session count as the only success criterion on a shared Grid. Another job can create or close sessions between queries, and a provider may not expose Grid's GraphQL API. Per-session evidence is stronger. Use the aggregate count only when the environment is isolated.

The practical standard is simple: one component creates the session, the same component defines when it ends, and every completed attempt leaves evidence tied to the session ID. With that ownership visible in code, an assertion can fail loudly without leaving a silent browser behind.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 7, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official w3.org reference

    w3.org

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Can WebDriver be used directly in Java try-with-resources?

No. Selenium's WebDriver interface does not implement AutoCloseable, so a driver cannot be the resource expression by itself. Put it inside a small owning wrapper whose close method calls quit.

Does driver.close end the whole Selenium session?

Usually it closes only the current top-level browsing context. The remote session can remain alive when another window exists, while quit requests deletion of the entire session and closes every associated window.

What happens if the test and driver.quit both throw?

Java keeps the exception from the try body as the primary failure and attaches the close failure as a suppressed exception. Reporters must print suppressed exceptions or the cleanup problem will be easy to miss.

How do I prove a remote WebDriver session was cleaned up?

Capture the session ID before leaving the resource scope, then search structured Grid events for the matching session deletion. On an isolated CI Grid, a GraphQL session count returning to zero is a useful additional check.

Should a shared suite-level driver use this wrapper?

A lexical wrapper is a poor fit when the session intentionally outlives one test method. Give the wrapper to the suite-level owner instead, or keep explicit lifecycle hooks until the suite is redesigned around one session per test.