Skip to main content
SeleniumDecoded

Browser and Driver Logs

Turn on the logs that explain failures: browser console output via BiDi, chromedriver and geckodriver logs, Selenium's own client logging, network failures, and how to collect them per test.

Selenium 4 Stable Updated 9 Sept 2026 · Verified against Selenium 4.48.0

A screenshot shows what the page looked like; logs show why. There are four sources, each answering a different question:

LogAnswersHow to get it
Browser consoleDid the app throw or log an error?BiDi script handlers (all browsers) or the legacy browser log type (Chromium only)
Driver logWhat did chromedriver or geckodriver do with each command?Service log options
Selenium client logWhat did the binding send and receive?Language logging framework
NetworkWhich requests failed or were slow?BiDi network events

Browser Console via BiDi

The cross-browser way and the one to use in new code. Register handlers once per driver and collect into a list you can attach to the report or assert on.

Collect console output for the whole test
Selenium 4 Stable
// options.setCapability("webSocketUrl", true) when creating the driver
public final class ConsoleCollector {
private final List<String> entries = new CopyOnWriteArrayList<>();
public ConsoleCollector(RemoteWebDriver driver) {
driver.script().addConsoleMessageHandler(e ->
entries.add(String.format("%s [%s] %s", e.getTimestamp(), e.getLevel(), e.getText())));
driver.script().addJavaScriptErrorHandler(e ->
entries.add(String.format("%s [uncaught] %s%n%s", e.getTimestamp(), e.getText(), e.getStackTrace())));
}
public List<String> errors() {
return entries.stream().filter(s -> s.contains("[error]") || s.contains("[uncaught]")).toList();
}
public String dump() { return String.join(System.lineSeparator(), entries); }
}
// In a test
ConsoleCollector console = new ConsoleCollector(driver);
// ... scenario ...
assertTrue("Console errors:\n" + console.dump(), console.errors().isEmpty());
# options.enable_bidi = True
class ConsoleCollector:
def __init__(self, driver):
self.entries = []
driver.script.add_console_message_handler(
lambda e: self.entries.append(f"{e.timestamp} [{e.level}] {e.text}"))
driver.script.add_javascript_error_handler(
lambda e: self.entries.append(f"{e.timestamp} [uncaught] {e.text}\n{e.stack_trace}"))
@property
def errors(self):
return [s for s in self.entries if "[error]" in s or "[uncaught]" in s]
def dump(self):
return "\n".join(self.entries)
# pytest fixture
@pytest.fixture
def console(driver):
return ConsoleCollector(driver)
def test_checkout(driver, console):
...
assert not console.errors, "Console errors:\n" + console.dump()
// new chrome.Options().enableBidi()
class ConsoleCollector {
entries = [];
static async attach(driver) {
const c = new ConsoleCollector();
await driver.script().addConsoleMessageHandler((e) => c.entries.push(`${e.timestamp} [${e.level}] ${e.text}`));
await driver.script().addJavaScriptErrorHandler((e) => c.entries.push(`${e.timestamp} [uncaught] ${e.text}\n${e.stackTrace}`));
return c;
}
get errors() { return this.entries.filter((s) => s.includes('[error]') || s.includes('[uncaught]')); }
dump() { return this.entries.join('\n'); }
}
const console = await ConsoleCollector.attach(driver);
// ... scenario ...
assert.deepStrictEqual(console.errors, [], 'Console errors:\n' + console.dump());
// new ChromeOptions { UseWebSocketUrl = true }
public sealed class ConsoleCollector
{
private readonly ConcurrentQueue<string> _entries = new();
public static async Task<ConsoleCollector> AttachAsync(IWebDriver driver)
{
var c = new ConsoleCollector();
var bidi = await driver.AsBiDiAsync();
await bidi.Log.OnEntryAddedAsync(e => c._entries.Enqueue($"{e.Timestamp} [{e.Level}] {e.Text}"));
return c;
}
public IEnumerable<string> Errors => _entries.Where(s => s.Contains("[error]"));
public string Dump() => string.Join(Environment.NewLine, _entries);
}
var console = await ConsoleCollector.AttachAsync(driver);
// ... scenario ...
Assert.That(console.Errors, Is.Empty, "Console errors:\n" + console.Dump());

The legacy driver.manage().logs().get("browser") API still works on Chromium and returns everything since the last call, but it is Chromium-only and not part of the W3C specification; Java removed its deprecated logging classes in 4.45. Treat it as a fallback for old suites.

Driver Logs: chromedriver and geckodriver

When Selenium says element not interactable and you disagree, the driver log tells you what the driver actually checked. Enable it through the service builder; write to a file per test run.

Verbose driver logging to a file
Selenium 4 Stable
// Chrome
ChromeDriverService chromeService = new ChromeDriverService.Builder()
.withLogLevel(ChromiumDriverLogLevel.DEBUG) // ALL, DEBUG, INFO, WARNING, SEVERE, OFF
.withLogFile(new File("target/logs/chromedriver.log"))
.withAppendLog(true)
.withReadableTimestamp(true)
.build();
WebDriver chrome = new ChromeDriver(chromeService, new ChromeOptions());
// Firefox
GeckoDriverService geckoService = new GeckoDriverService.Builder()
.withLogLevel(FirefoxDriverLogLevel.TRACE) // TRACE, DEBUG, CONFIG, INFO, WARN, ERROR, FATAL
.withLogFile(new File("target/logs/geckodriver.log"))
.withTruncatedLogs(false) // keep long messages intact
.build();
WebDriver firefox = new FirefoxDriver(geckoService, new FirefoxOptions());
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
# Chrome
chrome_service = ChromeService(
log_output="logs/chromedriver.log",
service_args=["--log-level=DEBUG", "--append-log", "--readable-timestamp"],
)
chrome = webdriver.Chrome(service=chrome_service)
# Firefox
firefox_service = FirefoxService(
log_output="logs/geckodriver.log",
service_args=["--log", "trace", "--log-no-truncate"],
)
firefox = webdriver.Firefox(service=firefox_service)
# Log to stdout instead (useful in CI): log_output=sys.stdout
const chrome = require('selenium-webdriver/chrome');
const firefox = require('selenium-webdriver/firefox');
// Chrome
const chromeService = new chrome.ServiceBuilder()
.loggingTo('logs/chromedriver.log')
.enableVerboseLogging();
const chromeDriver = await new Builder().forBrowser('chrome').setChromeService(chromeService).build();
// Firefox
const ffService = new firefox.ServiceBuilder().enableVerboseLogging(true); // trace level
const ffDriver = await new Builder().forBrowser('firefox').setFirefoxService(ffService).build();
// Chrome
var chromeService = ChromeDriverService.CreateDefaultService();
chromeService.LogPath = "logs/chromedriver.log";
chromeService.EnableVerboseLogging = true;
chromeService.EnableAppendLog = true;
var chrome = new ChromeDriver(chromeService, new ChromeOptions());
// Firefox
var ffService = FirefoxDriverService.CreateDefaultService();
ffService.LogLevel = FirefoxDriverLogLevel.Trace;
ffService.LogPath = "logs/geckodriver.log";
var firefox = new FirefoxDriver(ffService, new FirefoxOptions());

Driver logs at DEBUG or TRACE are large (tens of MB per test). Enable them for a specific failing test, not the whole suite, and delete them after reading.

Reading a chromedriver log: each command appears as [COMMAND] with its JSON, then [RESPONSE]. For a click, look for the interactability check and the coordinates it computed; if another element is at those coordinates, you have found your overlay.

Selenium Client Logging

The binding’s own log shows the HTTP traffic it sends: the command, the request body, the response. It is the source of truth when you suspect the binding, a proxy or a Grid is misbehaving.

Log the WebDriver protocol traffic
Selenium 4 Stable
// Selenium Java uses java.util.logging
import java.util.logging.*;
Logger root = Logger.getLogger("org.openqa.selenium");
root.setLevel(Level.FINE);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
root.addHandler(handler);
// Or route to SLF4J/Logback with the jul-to-slf4j bridge and configure there.
// Level FINE shows each command; FINER includes request/response bodies.
import logging
logging.basicConfig(level=logging.WARNING)
logging.getLogger("selenium").setLevel(logging.DEBUG)
# Just the wire protocol:
logging.getLogger("selenium.webdriver.remote.remote_connection").setLevel(logging.DEBUG)
# Also useful: urllib3 to see HTTP connection reuse and retries
logging.getLogger("urllib3").setLevel(logging.DEBUG)
const { logging } = require('selenium-webdriver');
// Log every command and response to stderr
logging.installConsoleHandler();
logging.getLogger('webdriver.http').setLevel(logging.Level.ALL);
// Or set SELENIUM_REMOTE_LOGGING or DEBUG=selenium-webdriver:* depending on version
// .NET binding logs through OpenQA.Selenium.Internal.Logging (4.12+)
using OpenQA.Selenium.Internal.Logging;
Log.SetLevel(LogEventLevel.Trace); // Trace shows request and response bodies
Log.Handlers.Add(new FileLogHandler("logs/selenium.log"));
// Console: Log.Handlers.Add(new ConsoleLogHandler());

Network Failures via BiDi

A 500 from an API explains far more failures than a missing element does. Collect failed responses the same way as console output.

Record failed requests
Selenium 4 Stable
import org.openqa.selenium.bidi.module.Network;
Network network = new Network(driver);
List<String> failures = new CopyOnWriteArrayList<>();
network.onResponseCompleted(e -> {
long status = e.getResponseData().getStatus();
if (status >= 400) failures.add(status + " " + e.getRequest().getMethod() + " " + e.getResponseData().getUrl());
});
network.onFetchError(e -> failures.add("FETCH ERROR " + e.getRequest().getUrl() + ": " + e.getErrorText()));
// After the scenario:
if (!failures.isEmpty()) System.out.println("Network failures:\n" + String.join("\n", failures));
failures = []
def on_response(response):
if response.status >= 400:
failures.append(f"{response.status} {response.url}")
driver.network.add_response_handler(on_response)
# After the scenario:
if failures:
print("Network failures:\n" + "\n".join(failures))
const { Network } = require('selenium-webdriver/bidi/network');
const network = await Network(driver);
const failures = [];
await network.responseCompleted((e) => {
if (e.response.status >= 400) failures.push(`${e.response.status} ${e.request.method} ${e.response.url}`);
});
await network.fetchError((e) => failures.push(`FETCH ERROR ${e.request.url}: ${e.errorText}`));
var bidi = await driver.AsBiDiAsync();
var failures = new ConcurrentBag<string>();
await bidi.Network.OnResponseCompletedAsync(e =>
{
if (e.Response.Status >= 400) failures.Add($"{e.Response.Status} {e.Request.Method} {e.Response.Url}");
});
await bidi.Network.OnFetchErrorAsync(e => failures.Add($"FETCH ERROR {e.Request.Url}: {e.ErrorText}"));

Putting It in the Failure Hook

Combine everything into one evidence bundle written when a test fails: screenshot, page source, console entries, network failures, and the driver log path. The Reporting lesson shows how to attach that bundle to Allure or Extent; the AI-Assisted Testing lesson shows the file-based version.

Summary

  • Console logs and JavaScript errors: BiDi script handlers, cross-browser; assert that there are none.
  • Driver logs: service builders, DEBUG or TRACE, file per run, enable only when hunting a specific failure.
  • Client logs: your language’s logging framework at DEBUG on the Selenium logger to see protocol traffic.
  • Network failures: BiDi network events, collected per test.
  • Bundle all of it in the failure hook so nobody has to rerun a failed test to understand it.

Related lessons