Error handling in BAP
BAP raises typed errors, so a dead WebSocket, a slow page, and a failed BrowserQL mutation each surface as a different class. This guide shows how to catch and branch on each type, tune the timeouts that produce them, and read the GraphQL errors BrowserQL returns.
- A Browserless API token from your account dashboard
- The BAP SDK installed and connecting, from the BAP Quickstart
The error types
Each failure gets its own class, so an instanceof check in TypeScript or an except clause in Python tells you which stage failed. Python's errors also share the BAPError base for catch-all handling.
- TypeScript
- Python
| Error | Thrown when |
|---|---|
ConnectionError | The WebSocket fails to open, drops mid-session, or is unavailable |
TimeoutError | An operation exceeds its transport deadline |
BrowserQLError | The BrowserQL server returns GraphQL errors. The errors property holds the raw GraphQL error objects |
All three are exported from @browserless.io/bap-ts alongside the default export.
| Error | Raised when |
|---|---|
BAPError | Base class for every error the SDK produces |
ConnectionError | The transport fails to connect or disconnects |
TimeoutError | An operation exceeds its transport deadline |
BrowserQLError | The GraphQL operation returned one or more errors |
ValueError | A public SDK argument failed client-side validation |
SchemaMismatchError | The generated schema metadata and runtime call site disagree, which usually means the installed package is out of date for the server you're talking to |
ConnectionError and TimeoutError share names with Python's builtins. Import them from bap explicitly, because an except clause written for the builtin will not catch the SDK's version. The generated errors reference is the authoritative list.
Catch and branch on error type
Branching on type tells you what to do next: reconnect on ConnectionError, raise the deadline or fix the selector on TimeoutError, and read the server's message on BrowserQLError.
- TypeScript
- Python
import Browserless, {
BrowserQLError,
ConnectionError,
TimeoutError,
} from "@browserless.io/bap-ts";
const TOKEN = "YOUR_API_TOKEN_HERE";
const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
});
try {
const page = await browser.newPage();
await page.goto("https://example.com", { timeout: 5000 });
} catch (error) {
if (error instanceof TimeoutError) {
console.error("operation timed out");
} else if (error instanceof ConnectionError) {
console.error("websocket connection failed");
} else if (error instanceof BrowserQLError) {
// The raw GraphQL error objects from the server.
console.error("browserql returned errors:", error.errors);
}
} finally {
await browser.close();
}
import bap.sync_api as bap
from bap import BrowserQLError, ConnectionError, TimeoutError
TOKEN = "YOUR_API_TOKEN_HERE"
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
token=TOKEN,
) as browser:
try:
with browser.page() as page:
page.goto("https://example.com", timeout=5000)
except TimeoutError:
print("operation timed out")
except ConnectionError:
print("websocket connection failed")
except BrowserQLError as error:
print("browserql returned errors:", error)
Response on a timeout:
operation timed out
Timeouts
Every operation runs against a deadline of 30000 milliseconds unless you change it. There are three places to do that, and the more specific one wins:
- Per connection. The
timeoutoption onBrowserless.connect()replaces the default for every operation on that browser. - Per session.
preferences({ timeout })replaces the session's defaults from inside the page, which is the option to reach for when you're driving a session you didn't open, such as one you reconnected to. - Per call. Navigation, selector, and CAPTCHA methods accept their own
timeoutoption, which overrides both of the above for that one call.
- TypeScript
- Python
// Every operation on this browser gets 60 seconds.
const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
timeout: 60_000,
});
const page = await browser.newPage();
// This one call gets 10 seconds, overriding the 60 above.
await page.waitForSelector(".results", { timeout: 10_000 });
await browser.close();
# Every operation on this browser gets 60 seconds.
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
token=TOKEN,
timeout=60000,
) as browser:
with browser.page() as page:
# This one call gets 10 seconds, overriding the 60 above.
page.wait_for_selector(".results", timeout=10000)
Set the connection-level timeout to the slowest operation you consider healthy, then lower it per call where you want to fail fast. A short deadline on waitForSelector catches a missing element in seconds instead of holding the session for the full default.
How BrowserQL failures surface
BAP is a typed wrapper over BrowserQL, and BrowserQL reports failures in a GraphQL errors array rather than through HTTP status codes. When a mutation your method call maps to comes back with errors, the SDK raises BrowserQLError carrying those raw error objects. The message text is written by the BrowserQL server, so it names the mutation and the reason, such as a selector that never matched or a navigation that was blocked.
Two consequences of the GraphQL model are worth knowing before you debug:
- Navigation doesn't throw on HTTP error statuses.
goto()resolves with the response object, so a 404 or 500 from the target site is data, not an exception. Checkresponse.statuswhen you need to react to it. - The server decides what's fatal. Some conditions BrowserQL reports as errors are recoverable from the session's point of view. The BQL error handling guide covers how the underlying protocol behaves, which is what
BrowserQLErroris wrapping.
- TypeScript
- Python
const response = await page.goto("https://example.com/missing-page");
// A 404 arrives here, not in a catch block.
if (response && response.status >= 400) {
console.warn(`target returned ${response.status}`);
}
response = page.goto("https://example.com/missing-page")
# A 404 arrives here, not in an except block.
if response and response.status >= 400:
print(f"target returned {response.status}")
Retrying failed operations
ConnectionError is the type worth retrying, because a dropped socket says nothing about your script's logic. Reconnect by creating a fresh page rather than reusing the dead one, since each page owns its own WebSocket. TimeoutError and BrowserQLError usually mean the page or the script needs to change, so blind retries tend to fail the same way while spending your concurrency.
One constraint on what you retry: the server may have executed a mutation even though its response never reached you, so a retried callback can repeat a click or a form submission. Pass only operations that are safe to run twice, such as navigate-and-read, and reconcile anything non-idempotent explicitly before retrying it.
- TypeScript
- Python
import Browserless, { ConnectionError } from "@browserless.io/bap-ts";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function withRetry<T>(run: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await run();
} catch (error) {
// Only a dropped socket is worth retrying; other errors repeat identically.
if (!(error instanceof ConnectionError) || attempt === attempts) throw error;
// Back off between attempts so retries don't hammer a busy service.
await sleep(1000 * 2 ** (attempt - 1));
}
}
}
const title = await withRetry(async () => {
const page = await browser.newPage();
try {
await page.goto("https://example.com");
return await page.title();
} finally {
// Every page holds its own socket and concurrency slot, so close it
// whether the attempt succeeded or not.
await page.close();
}
});
import time
from bap import ConnectionError
def with_retry(run, attempts=3):
for attempt in range(1, attempts + 1):
try:
return run()
except ConnectionError:
# Only a dropped socket is worth retrying; other errors repeat identically.
if attempt == attempts:
raise
# Back off between attempts so retries don't hammer a busy service.
time.sleep(2 ** (attempt - 1))
def visit():
# The context manager closes the page even when the attempt fails.
with browser.page() as page:
page.goto("https://example.com")
return page.title()
title = with_retry(visit)
The retry with exponential backoff example expands this pattern with jitter and queue-aware handling. If the session itself must survive a client-side crash or restart, reconnect to the running session instead of starting over.
FAQ & Troubleshooting
Why did my operation time out after exactly 30 seconds?
30000 milliseconds is the default per-operation timeout. Raise it for the whole browser with timeout on Browserless.connect(), or per call with the timeout option on navigation, selector, and CAPTCHA methods. The per-call value wins when both are set.
(Python) My except ConnectionError block never fires
You're catching Python's builtin ConnectionError, not the SDK's. Add from bap import ConnectionError to the module doing the catching. The same applies to TimeoutError.
goto() didn't throw even though the page returned a 500
That's by design. BrowserQL reports HTTP status through the response object rather than as an error, so a failing target site is data you inspect with response.status. Only transport failures, timeouts, and GraphQL errors throw.
What's inside a BrowserQLError?
The raw GraphQL error objects the BrowserQL server returned, on the errors property in TypeScript and in the exception message in Python. Each one names the mutation that failed and why. The BQL error handling guide explains the underlying error format.