Skip to content

Error handling guide — bidiwave

Exception hierarchy

BiDiError (base)
├── BiDiConnectionError      # WebSocket disconnected or unreachable
├── BiDiTimeoutError         # Timeout waiting for response or navigation
├── CapabilityError          # Browser does not support the capability
├── ProtocolError            # Invalid or unexpected protocol message
├── SessionError             # Invalid or expired session
└── CommandError             # Browser rejected the command
    ├── InvalidArgumentError # Invalid argument
    ├── NoSuchFrameError     # Context not found
    └── JavaScriptError      # Error evaluating JS

Errors by scenario

Connection

Scenario Exception Common cause Solution
Browser not responding BiDiConnectionError Browser not launched, wrong port Verify the browser is running with --remote-debugging-port
WebSocket closed BiDiConnectionError Browser crashed or was closed Automatic reconnect, or reconnect manually
Timeout on connect BiDiTimeoutError Firewall, slow network Increase timeout in BiDiClient.connect()

Session

Scenario Exception Common cause Solution
session.new fails SessionError Unsupported capabilities Verify alwaysMatch in capabilities
Session expired SessionError Session closed by the browser Create a new session
session.subscribe fails CommandError Unsupported event type Verify client.capabilities
Scenario Exception Common cause Solution
Invalid URL CommandError Malformed URL Validate URL before navigating
Load timeout BiDiTimeoutError Slow or hanging page Use wait="none" or wait="interactive", or increase timeout
Context closed CommandError Context was closed by another process Verify the context exists before navigating
Screenshot fails CommandError Context not visible (headless without GPU) Use --headless=new in Chrome

Script

Scenario Exception Common cause Solution
JS error CommandError Invalid expression Catch errors in JS: try { ... } catch(e) { return e.message }
Promise not resolved BiDiTimeoutError await_promise=True and Promise hung Use timeout in JS: Promise.race([promise, timeout(5000)])
Invalid handle CommandError Handle already released or context closed Don't reuse handles after disown() or close()

Events

Scenario Exception Common cause Solution
Handler fails (silent) Exception in async handler The handler runs in error isolation. Log errors inside the handler
No events received (silent) Not subscribed or context closed Verify session.subscribe() and that the context is active

Reconnection

Scenario Exception Common cause Solution
Reconnect fails BiDiConnectionError Browser closed Handle on_reconnect failure, relaunch browser
Session lost after reconnect SessionError BiDi session does not survive reconnection Create new session in on_reconnect handler
Duplicate handlers after reconnect (silent) Re-registering handlers on each reconnect Handlers are kept in memory, no need to re-register

Error handling patterns

Retry with backoff

async def navigate_with_retry(client, ctx, url, max_retries=3):
    for attempt in range(max_retries):
        try:
            return await client.browsing.navigate(ctx, url, wait="complete")
        except BiDiTimeoutError:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)

Context-safe operations

async def safe_navigate(client, ctx, url):
    try:
        return await client.browsing.navigate(ctx, url, wait="complete")
    except CommandError as e:
        if "no such frame" in e.message.lower():
            ctx = await client.browsing.create_context()
            return await client.browsing.navigate(ctx, url, wait="complete")
        raise

Capability guard

async def safe_screenshot(client, ctx):
    if not client.capabilities.supports_browsing:
        raise CapabilityError("Browser does not support browsing.captureScreenshot")
    return await client.browsing.screenshot(ctx)

Promise with timeout in JS

result = await client.script.evaluate(
    ctx,
    """new Promise((resolve, reject) => {
        setTimeout(() => resolve('done'), 1000);
        setTimeout(() => reject(new Error('timeout')), 5000);
    })""",
    await_promise=True,
)

Logging

bidiwave uses Python's standard logging module. Configure to see messages:

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("bidiwave").setLevel(logging.DEBUG)

Levels:

  • DEBUG — all messages sent/received, ID correlation
  • INFO — connections, reconnections, subscriptions
  • WARNING — events dropped (backpressure), failed handlers
  • ERROR — protocol errors, rejected commands