PRACTICAL GUIDE / InvalidSessionIdException after driver close

Why Selenium loses the session after you close a window

Diagnose Selenium session loss after close(), separate last-window failures from teardown races, and make driver cleanup safe in local and CI runs.

By The Testing AcademyUpdated August 7, 202622 min read
All field guides
In this guide7 sections
  1. Why one close call can remove the whole session
  2. Follow the command order before changing teardown
  3. Fix window ownership where the close is issued
  4. Separate last-window loss from teardown races and crashes
  5. A stale driver object can produce the same exception
  6. Make teardown single-owner and safe in CI
  7. Know when not to close a window

What you will learn

  • Why one close call can remove the whole session
  • Follow the command order before changing teardown
  • Fix window ownership where the close is issued
  • Separate last-window loss from teardown races and crashes

Your test closes a popup, then the next assertion fails because Selenium says the session no longer exists. The close call looked harmless and returned without an error. What mattered was not that a window closed, but whether any window was left in that browser session.

That distinction gets lost in suites where page objects close windows, teardown always calls quit, and parallel workers share a driver reference. The exception arrives late, so the last line in the stack trace often gets blamed for a session that died several commands earlier.

Why one close call can remove the whole session

A WebDriver session lives at the remote end, which may be a local driver process or a Selenium Grid node. The Java WebDriver object is only the client-side handle. Each command carries a session identifier to the remote end. Finding an element, reading a title, taking a screenshot, and switching windows all depend on that identifier still mapping to an active session.

The close and quit operations are not synonyms. close() asks the remote end to close the current top-level browsing context, usually a tab or window. quit() asks it to delete the session. The important edge case is defined by WebDriver itself: after the current window closes, the remote end checks whether any top-level browsing contexts remain. If none remain, it closes the session.

That makes this sequence valid:

  1. The test starts in the main application window.
  2. A click opens a separate payment window.
  3. The test switches to the payment window.
  4. close() closes that payment window.
  5. The test switches back to the saved main-window handle.
  6. Commands continue in the same session.

This superficially similar sequence kills the session:

  1. The browser still has only the original window.
  2. A helper assumes it is looking at a popup.
  3. close() closes that original window.
  4. No top-level browsing context remains.
  5. The remote end deletes the session.
  6. getTitle(), findElement(), or quit() becomes the first command to reveal the loss.

Java adds a naming wrinkle. The wire protocol describes the response as an invalid session id error. Selenium's Java API documents NoSuchSessionException for commands called after quit(). Depending on the command path, remote end, and binding version, teams may talk about either name. Do not build diagnosis around the exception label alone. The useful fact is that the command was sent with a session ID that the remote end no longer recognizes.

Here is a small program that demonstrates the surviving-window outcome without relying on a test framework. It saves the original handle before creating a second window, verifies the count before closing, and switches explicitly after the close.

Java
import java.time.Duration;
import java.util.Set;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WindowType;
import org.openqa.selenium.chrome.ChromeDriver;

public final class WindowLifecycleExample {
    public static void main(String[] args) {
        WebDriver driver = new ChromeDriver();
        try {
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));
            driver.get("https://www.selenium.dev/selenium/web/web-form.html");

            String mainWindow = driver.getWindowHandle();
            driver.switchTo().newWindow(WindowType.TAB);
            driver.get("https://www.selenium.dev/documentation/");

            Set<String> beforeClose = driver.getWindowHandles();
            if (beforeClose.size() != 2) {
                throw new IllegalStateException(
                    "Expected two windows, found " + beforeClose.size());
            }

            driver.close();
            driver.switchTo().window(mainWindow);

            String heading = driver.findElement(By.tagName("h1")).getText();
            System.out.println("Still using the original session: " + heading);
        } finally {
            driver.quit();
        }
    }
}

The explicit count is more than defensive programming. It states the precondition for close(): another window must exist and the code must know which handle it will use next. Without that contract, close() is a lifecycle operation hidden behind a convenience method.

Window handles are session-scoped strings. A handle from an old session cannot be applied to a new one. Starting a replacement driver inside a catch block therefore does not repair the flow. It creates a different browser with different storage, cookies, history, downloads, permissions, and server-side application state. If the test continues, its later assertions no longer describe the journey that failed.

Follow the command order before changing teardown

The failure line tells you where the missing session was noticed, not where it was deleted. Build a short timeline using the test ID, session ID, thread name, current handle, and handle count. Capture those values immediately before every close or quit while investigating. Do not call extra WebDriver methods after the exception just to improve the log, because those diagnostic calls need the same dead session.

RemoteWebDriver exposes getSessionId() in Java. That is useful at framework boundaries, but page objects should not need to cast the driver merely to log ownership. Put the instrumentation in a driver fixture or a narrow lifecycle wrapper. The following class records the state before closing and refuses to close the sole remaining window.

Java
import java.time.Instant;
import java.util.Set;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;

public final class WindowCloser {
    private WindowCloser() {}

    public static void closeSecondaryWindow(WebDriver driver, String returnTo) {
        Set<String> handles = driver.getWindowHandles();
        String current = driver.getWindowHandle();
        String session = driver instanceof RemoteWebDriver remote
            ? remote.getSessionId().toString()
            : "not-exposed";

        System.out.printf(
            "%s action=close-window session=%s thread=%s current=%s handles=%s returnTo=%s%n",
            Instant.now(),
            session,
            Thread.currentThread().getName(),
            current,
            handles,
            returnTo
        );

        if (handles.size() < 2) {
            throw new IllegalStateException(
                "Refusing to close the only open window; current=" + current);
        }
        if (!handles.contains(returnTo) || current.equals(returnTo)) {
            throw new IllegalArgumentException(
                "Return handle must name a different, open window");
        }

        driver.close();
        driver.switchTo().window(returnTo);
    }
}

A useful failure record looks like this:

Shell
2026-08-04T09:42:18.431Z action=close-window session=8d0f4b7e thread=pool-2-thread-3 current=CDwindow-A handles=[CDwindow-A] returnTo=CDwindow-MAIN
org.openqa.selenium.NoSuchSessionException: invalid session id
Build info: version: '4.x'
Driver info: org.openqa.selenium.remote.RemoteWebDriver
Command: [8d0f4b7e, getTitle {}]

The first line already settles the last-window hypothesis. The alleged return handle was not in the observed handle set, and the current window was the only one open. The getTitle command is a victim.

Compare that with a teardown race:

Shell
09:42:18.431 test=checkout-a action=quit session=8d0f4b7e thread=pool-2-thread-3
09:42:18.447 test=checkout-b action=find session=8d0f4b7e thread=pool-2-thread-4
09:42:18.451 test=checkout-b result=invalid-session-id session=8d0f4b7e

Here, no close operation is required. Two tests used the same session ID on different threads. One owner's teardown deleted it while the other test was active. Adding a window-count guard would not fix shared ownership.

Grid evidence can answer a third possibility. Query Grid status while the test is running and retain the response near the failure. The public status endpoint lists nodes, slots, sessions, and capabilities. If the session disappears immediately after the close call while the node remains UP, lifecycle deletion is plausible. If the node itself becomes unavailable and multiple unrelated sessions fail together, investigate node or network loss instead.

Shell
GRID_URL=http://localhost:4444
curl --fail --silent "$GRID_URL/status" |
  python -m json.tool > "grid-status-$(date +%s).json"

curl --fail --silent \
  -H "Content-Type: application/json" \
  --data '{"query":"{ grid { sessionCount sessionQueueSize } nodesInfo { nodes { id uri status sessionCount slotCount } } }"}' \
  "$GRID_URL/graphql" |
  python -m json.tool

Correlate timestamps carefully. A status snapshot taken thirty seconds after teardown says little about the state at failure. Structured Grid logs or traces are better when available because they preserve command and session context. Screenshots are weak evidence here. A successful screenshot proves the session existed when that screenshot command ran. A missing screenshot after the error proves only that another command also reached a dead session.

Browser process logs can separate a user-initiated or application-initiated close from a WebDriver close call. For example, the application may execute window.close() in a popup, a browser policy may terminate a window, or the driver process may crash. Your lifecycle log should therefore record intent ("test called close") separately from observation ("handle count changed"). Treat those as two different facts.

Fix window ownership where the close is issued

The clean fix is not a broad exception handler. Give the code that opens a secondary window responsibility for identifying it, using it, closing it, and returning to a known surviving handle. A page object may expose business actions, but the test or a dedicated window helper should own this browser-level transition.

Waiting for "any second handle" is also too loose when advertisements, authentication brokers, or application telemetry can open windows. Compute the set difference between handles observed before and after the action. Then require exactly one new handle. That produces a meaningful failure if no window opens or several appear.

Java
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;

public final class NewWindow {
    private NewWindow() {}

    public static String waitForOneNewHandle(
            WebDriver driver, Set<String> originalHandles) {
        return new WebDriverWait(driver, Duration.ofSeconds(10)).until(d -> {
            Set<String> added = new HashSet<>(d.getWindowHandles());
            added.removeAll(originalHandles);
            if (added.size() > 1) {
                throw new IllegalStateException(
                    "Expected one new window, found " + added);
            }
            return added.size() == 1 ? added.iterator().next() : null;
        });
    }

    public static void useAndClose(
            WebDriver driver, Runnable opensWindow, Runnable assertions) {
        String original = driver.getWindowHandle();
        Set<String> before = driver.getWindowHandles();

        opensWindow.run();
        String secondary = waitForOneNewHandle(driver, before);
        driver.switchTo().window(secondary);

        try {
            assertions.run();
        } finally {
            Set<String> now = driver.getWindowHandles();
            if (now.contains(secondary) && now.size() > 1) {
                driver.close();
            }
            if (driver.getWindowHandles().contains(original)) {
                driver.switchTo().window(original);
            }
        }
    }
}

This helper has an intentional cost. It adds a wait at the boundary and rejects ambiguous multi-window behavior. A test that legitimately opens two windows needs a more specific selection rule, perhaps a title, URL, or application-provided marker after switching among the new handles. The stricter failure is useful, but adopting it may reveal product behavior the old suite silently ignored.

There is another trap in the finally block: the secondary window may already have closed itself. Calling driver.close() blindly would then close whichever window Selenium currently considers active, possibly the original. That is why the helper checks whether the secondary handle still exists. Even so, window state can change between the check and the command. Tests for highly dynamic multi-window applications need application-specific coordination rather than pretending the operation is atomic.

Consider a single-window login flow. Some identity providers redirect the same tab instead of opening a popup based on browser settings or mobile emulation. A generic "close login window" helper will kill that session. The right assertion is about the navigation model before any close call:

Java
Set<String> before = driver.getWindowHandles();
String original = driver.getWindowHandle();

loginPage.startSingleSignOn();

String authenticatedUrl = new WebDriverWait(driver, Duration.ofSeconds(15))
    .until(d -> d.getCurrentUrl().contains("/account")
        ? d.getCurrentUrl()
        : null);

if (driver.getWindowHandles().equals(before)
        && driver.getWindowHandle().equals(original)) {
    System.out.println("Identity provider used same-window redirect: " + authenticatedUrl);
} else {
    throw new IllegalStateException(
        "This test expects same-window SSO; observed handles="
            + driver.getWindowHandles());
}

No close belongs in that test. The product chose navigation in the existing context, and the automation should model it accurately.

Separate last-window loss from teardown races and crashes

Several failures converge on the same invalid-session response. Treat them as competing hypotheses and look for evidence that only one can explain.

A last-window close has a recognizable chain: a recorded close intent, one handle immediately beforehand, a successful close response or abrupt window disappearance, then the session is absent. It usually affects one test. The browser and node may remain healthy for later sessions.

A shared-driver teardown race shows one session ID attached to two test identities, threads, or workers. A quit event from one test precedes a command from the other. Window counts may be perfectly normal. The correction is driver confinement, not window handling. Each parallel test needs its own driver and its own cleanup owner.

A node failure has a wider blast radius. Several session IDs on the same node fail in a narrow interval. Grid status may report the node as UNAVAILABLE, the node process may restart, or the connection may fail before a protocol response is returned. Depending on timing, clients can report connection errors, session-not-found errors after the Grid reconciles its map, or both. Do not turn that infrastructure incident into a test-level close guard.

A browser crash tends to leave driver or node logs about a lost browser process. The application under test can trigger it through resource exhaustion, but the diagnostic scope is still process health. Look for renderer crashes, out-of-memory termination, container eviction, or driver exit codes. A Java stack trace alone cannot prove the cause.

A session timeout or administrative deletion is another near-match. Grid operators may drain nodes, remove sessions, or restart components. Some providers impose idle timeouts. If there is a long gap between commands, compare it with provider and infrastructure limits. The key evidence is elapsed idle time and an external lifecycle event, not a close call.

A stale driver object can produce the same exception

A suite can have a healthy replacement session and still send the next command through a dead one. This happens when a page object, assertion helper, event listener, or retry callback keeps the WebDriver instance it received during construction. The fixture ends that instance normally, creates another for the next attempt, and updates its own reference. The retained helper does not change with it. Its first later command carries the deleted session identifier, so the remote end returns the same invalid session response seen after closing the last window.

This failure does not require parallel execution. It can occur on one thread with orderly teardown, which is what makes it easy to misclassify as a slow teardown race. A retry mechanism is a common place to expose it because the retry creates a second driver while reusing objects built for the first attempt. A test factory that constructs pages before the per-test fixture runs can create the same lifetime mismatch. The defect is not that the replacement session disappeared. The defect is that the caller never addressed that replacement session.

Separate this case with a session lineage, not a stack trace. Give each test attempt an owner identifier and record fixture creation, helper construction, fixture destruction, and the session identifier on the failing command. Consider the illustrative identifiers session-A and session-B. A healthy retry shows attempt one creating, commanding, and quitting session-A; attempt two then creates and commands session-B. A broken retry shows session-B being created for attempt two, followed by a page action whose command line still names session-A. That mismatch is decisive even if both actions run on the same thread and the current fixture field points to session-B.

Read the session field attached to the failed command first. In Java exception output, the useful portion is the identifier beside Command, not merely the Driver info line. Driver info can still say RemoteWebDriver for both the abandoned object and the current object, so it does not establish which instance was used. Next, find the most recent creation record for the same test and attempt. A healthy value is the same identifier in both records. A broken value is an older identifier on the command than on the attempt's creation record. A missing identifier on a log line means the logger did not capture enough evidence; it does not mean the command was sessionless.

Several values look reassuring while proving nothing about this hypothesis. A non-null fixture field proves only that Java holds an object. A Grid node status of UP proves the node can participate in the Grid, not that the failed command used the fixture's current session. A Grid-wide session count of one can be especially misleading: that one session may be session-B while the helper is still sending session-A. An earlier handle count of two is also historical data tied to the older object. It cannot describe the replacement browser unless the session identifier beside that count matches the replacement.

The most compact diagnostic record therefore needs the test ID, attempt number, fixture owner, lifecycle event, session identifier, and command sequence. Thread name remains useful, but it is secondary here. If the sequence reads create A, construct helper with A, quit A, create B, command A, investigate object lifetime and retry setup. If it reads create A, command A, and no fixture replacement, stale retention is not supported. Return to close intent, external deletion, or process-health evidence instead.

Do not fix this by making every helper look up a mutable global driver. That replaces a stale reference with cross-test ambiguity. Either rebuild the helper graph for each attempt or resolve the driver through a test-scoped owner that cannot return another test's instance. The first approach makes lifetimes obvious but forces retries to reconstruct page objects and their dependent components. The second centralizes validation but adds an access layer to every WebDriver boundary and makes unit tests supply that scope. Those are concrete maintenance costs, not reasons to tolerate commands crossing attempt boundaries.

The worst response to these cases is this:

Java
try {
    return driver.getTitle();
} catch (org.openqa.selenium.WebDriverException failure) {
    driver = new org.openqa.selenium.chrome.ChromeDriver();
    return driver.getTitle();
}

Besides losing state, this code catches unrelated WebDriver failures and mutates ownership in a place that cannot guarantee cleanup. It may make the current line green while leaking the replacement process. It also converts an infrastructure fault into a misleading application assertion. A session is a test resource, not a retryable HTTP request.

If retry is part of team policy, retry the whole independently runnable test with a newly provisioned fixture. Mark the original attempt as failed or flaky according to that policy, and keep its logs. Retry only after classification where possible. Re-running an unsafe test that closes arbitrary windows will simply reproduce the bug less consistently.

Make teardown single-owner and safe in CI

A reliable fixture has one creation point and one destruction point. It does not expose a global static driver to parallel tests. It does not call close() during generic cleanup. It preserves the primary test failure if quit() also fails.

JUnit 5 extensions can store the driver in the extension context associated with one test. The following compact extension uses a dedicated namespace, creates one browser before each test, and removes the reference before quit. Removing first makes a second cleanup call a no-op rather than a second command on the same session.

Java
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public final class DriverExtension
        implements BeforeEachCallback, AfterEachCallback {
    private static final ExtensionContext.Namespace NS =
        ExtensionContext.Namespace.create(DriverExtension.class);
    private static final String KEY = "driver";

    @Override
    public void beforeEach(ExtensionContext context) {
        context.getStore(NS).put(KEY, new ChromeDriver());
    }

    public static WebDriver driver(ExtensionContext context) {
        WebDriver driver = context.getStore(NS).get(KEY, WebDriver.class);
        if (driver == null) {
            throw new IllegalStateException("No driver belongs to this test");
        }
        return driver;
    }

    @Override
    public void afterEach(ExtensionContext context) {
        WebDriver driver =
            context.getStore(NS).remove(KEY, WebDriver.class);
        if (driver != null) {
            driver.quit();
        }
    }
}

Production fixtures often need richer exception handling because JUnit must retain the test's original failure. At minimum, log a quit failure with the test identifier and session identifier. Do not throw it over an already failing assertion without preserving both. If process cleanup is frequently failing, address that as an infrastructure reliability problem rather than permanently suppressing it.

CI should retain lifecycle logs on failed and cancelled runs. Cancellation matters because jobs are often stopped while browsers are active. The runner may send a termination signal before Java finally blocks finish. Grid-side idle cleanup remains necessary for those cases even when fixture code is correct.

YAML
name: selenium-lifecycle-check
on:
  pull_request:
jobs:
  window-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
      - name: Run window lifecycle tests
        run: ./mvnw -B -Dtest='*Window*Test' test
      - name: Keep lifecycle evidence
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: webdriver-lifecycle
          path: |
            target/surefire-reports/
            target/webdriver-lifecycle/
          if-no-files-found: warn

Roll the change out in stages. First add observation without changing behavior: session ID at fixture creation and teardown, test ID, thread, close intent, and handle set. Keep that logging short and machine-readable. Second, find session IDs associated with more than one concurrently active test. Fix shared ownership before adding close guards, or the guard may obscure the larger defect.

Third, replace general-purpose close helpers with operations named for a real secondary window, such as closePaymentWindowAndReturnToCheckout. Require a saved return handle. Fourth, make fixture cleanup use quit() once. Fifth, run the affected tests repeatedly under the same parallelism as CI. A serial local run cannot validate driver confinement.

Measure the rollout with counts that have operational meaning: sessions created, sessions quit by their owner, cleanup failures, close calls made with one handle, and session IDs seen under multiple test IDs. Avoid using "number of caught InvalidSessionIdException instances" as the only metric. Once an exception is swallowed, that number can improve while browser leaks and false passes increase.

For an established suite, retained references need their own migration order. Land attempt and session lineage before changing construction. Without that evidence, a retry that starts failing earlier looks like a regression even when the new check has exposed an old lifetime violation. Keep the initial instrumentation at fixture and helper boundaries rather than logging every WebDriver command. Per-command logging gives finer resolution, but it adds one record for every interaction and can turn a high-volume UI run into a storage and log-ingestion problem. Boundary records cost less, although a helper that quietly changes its delegate between boundaries can escape them.

Migrate the longest-lived objects first: static page registries, suite-level listeners, cached suppliers, and retry callbacks. They break before ordinary test-local page objects because their lifetime already exceeds one driver attempt. Screenshot and reporting listeners deserve an explicit decision. If they currently fetch the driver after teardown, moving driver removal earlier will stop those late artifacts. Capture failure evidence while the owning session is still available, then pass the completed artifact to reporting code. Do not keep a dead driver reachable merely so a reporter can try another command against it.

Next, move one representative test group to attempt-scoped construction. Rebuild every page object and browser-aware helper after the new fixture creates its session. Run that group under the CI retry and parallel settings, not only through a direct local invocation. The change is working when every command-bearing record for an attempt has the same session identifier as that attempt's creation record, every ended attempt has one owning cleanup event, and a retry begins with a different identifier. These invariants catch the wiring defect before a reduction in exception totals becomes statistically persuasive.

Only after the canary is clean should the framework reject a helper whose recorded attempt or session differs from the active owner. That rejection will move some failures from the remote end into framework code. Test authors may initially see more failures because previously hidden reuse is now deterministic. Land clear owner and attempt fields in the rejection message first, then enable the check across groups. A broad switch without those fields sends feature teams a new exception with less evidence than the Selenium error it replaced.

The test-framework team owns scoped driver creation, lineage records, and the rule that prevents an old helper from borrowing a new attempt. Feature teams own rebuilding their page objects at the supported boundary and removing suite-level caches. The Grid team owns the issue only when the command used the active fixture session and infrastructure evidence shows that session vanished without the fixture ending it. Routing every invalid-session report directly to Grid operations wastes the one distinction the client can prove.

A useful handoff contains the test and attempt IDs, complete ordered lifecycle records for both the expected and commanded session identifiers, the failing command name, the helper type that retained the driver, and whether a retry occurred. Include timestamps from one clock source and the relevant Grid or node event if infrastructure deletion is suspected. A stack trace without the fixture creation record cannot separate stale retention from remote deletion. A Grid snapshot without the command's session identifier cannot show that the snapshot describes the failed session.

This lineage technique does not catch a browser or renderer that is hung while its WebDriver session remains registered. In that case the expected and commanded identifiers match, the node can remain available, and no lifetime rule fires. Command-duration evidence, browser process logs, and timeout classification are needed for that failure. Session identity answers which browser the code addressed. It does not prove that browser can still make progress.

Know when not to close a window

Do not call close() in ordinary test teardown. quit() communicates the real intent and asks the remote end to release the complete session. Closing one window and abandoning the driver can leave other windows, the driver service, temporary profiles, and Grid slots alive until an external timeout.

Do not add a sleep after close(). Time cannot recreate a deleted session. A sleep may make a popup race less frequent, but it also lengthens every run and hides the missing synchronization condition. Wait for the new handle, a specific URL, or another observable state instead.

Do not probe the driver with getTitle() merely to decide whether it is alive and then continue. That check races with any other owner and adds a command that can itself fail. Ownership and lifecycle events are the reliable model. In a well-confined test, no other thread can delete the session between a health probe and the next command.

Do not keep a session alive solely to save browser startup time if doing so crosses test boundaries. Session reuse couples cookies, local storage, permissions, service workers, downloads, and server-side identity. It can reduce runtime, but the price is isolation and diagnosability. If a deliberately stateful journey spans several checks, model it as one test or one explicitly owned scenario rather than a global driver.

Do not wrap every quit() in an empty catch block. Idempotent cleanup means the owner can be invoked safely when setup was partial; it does not mean all cleanup failures are irrelevant. A failed quit can consume Grid capacity, leave a process running, and affect later jobs. Record it, attach it to the owning test, and alert when the rate becomes material.

There are legitimate reasons to use close(): validating multi-window behavior, dismissing a real secondary context, or testing how the application responds when that context disappears. Each reason has the same engineering cost. The test must identify the context it plans to close, prove another context should survive, switch deliberately, and keep one owner for the final quit. If that ceremony feels excessive, the test probably did not need close() in the first place.

// 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

Why does driver.close() sometimes end my Selenium session?

Closing the last open top-level browsing context causes the remote end to delete the WebDriver session. A later command then has no live session to address, even though close() itself may have returned normally.

Should I use close() or quit() in Selenium teardown?

Use quit() when the test is finished because it deliberately ends the session and releases its browser and driver resources. Reserve close() for a test that must dismiss one window while continuing in another known window.

Why do Java tests show NoSuchSessionException instead of InvalidSessionIdException?

Java bindings commonly represent a command sent after quit() as NoSuchSessionException, while the WebDriver protocol error is invalid session id. Treat the message, command order, and session identifier as stronger evidence than a cross-language class name.

Can I catch the session exception and create a new driver?

A replacement browser cannot recover the state, cookies, open pages, or evidence from the deleted session. Start a fresh test only at a framework-owned boundary, and record the original failure instead of hiding it inside a page object.

How do I make Selenium cleanup safe after a failed setup?

Keep driver ownership in one fixture, allow the reference to be absent, and call quit() once from that owner. Cleanup should record its own failure without replacing the test's primary exception.