Error Handling
This guide covers comprehensive error handling strategies for WebDriverBiDi.NET applications.
Overview
WebDriverBiDi.NET operations can fail for various reasons:
- Network connectivity issues
- Browser crashes or disconnections
- Invalid command parameters
- Timeout waiting for responses
- JavaScript exceptions in the browser
- Protocol-level errors
Understanding how to handle these errors properly is crucial for building robust automation.
Exception Types
Exception Hierarchy
Every protocol-level failure is reported through WebDriverBiDiException, so one catch clause covers everything the browser or the connection can do to you. Caller mistakes surface as the usual .NET exceptions instead, which are listed below after the table. The more specific protocol types let you react differently to different failures:
Exception
└── WebDriverBiDiException
├── WebDriverBiDiErrorResponseException (abstract)
│ ├── WebDriverBiDiCommandException
│ └── WebDriverBiDiProtocolException
├── WebDriverBiDiTimeoutException
├── WebDriverBiDiConnectionException
└── WebDriverBiDiSerializationException
| Type | Thrown when | Where you see it |
|---|---|---|
WebDriverBiDiCommandException |
The browser answers a command with an error response | From the command call (NavigateAsync, ExecuteCommandAsync, ...) |
WebDriverBiDiProtocolException |
The browser sends an error response that matches no pending command | Never from a command call; routed through UnexpectedErrorBehavior (ignored, collected, or thrown from the next command) |
WebDriverBiDiTimeoutException |
No response arrives within the command timeout; a connection does not open within its StartupTimeout; a send does not obtain exclusive access to the connection within its DataTimeout; or an operation does not obtain exclusive access to the connection within Transport.ConnectionLockTimeout (see Transport Connection Lock Timeout) |
From the command call, or from StartAsync or StopAsync |
WebDriverBiDiConnectionException |
Sending while not connected; starting an already-started driver; the connection drops while a command is in flight; the connection cannot be opened; or the connection is lost while the session is being established (see Losing the connection while connecting) | From the command call, or from StartAsync |
WebDriverBiDiSerializationException |
Command parameters cannot be serialized, or a response cannot be deserialized | From the command call. A malformed error response or event that belongs to no command is routed through ProtocolErrorBehavior instead, and a message that is not valid JSON through UnknownMessageBehavior |
WebDriverBiDiException (directly) |
A command is canceled or returns no result or a result of the wrong type; a duplicate command ID; a RemoteValue.As<T>() or EvaluateResult.As<T>() cast or a LocalValue conversion fails; event arguments of an unexpected type |
From the call that performed the cast, conversion, or command |
WebDriverBiDiErrorResponseException is the abstract base of the two types that carry a structured error from the browser. It exposes ErrorDetails (the raw ErrorResult), ErrorCode (the ErrorCode enum value, or ErrorCode.UnsetErrorCode for an unrecognized error string), ProtocolErrorType, ProtocolErrorMessage, and RemoteStackTrace. Prefer ErrorCode over inspecting Message when deciding how to react.
Library calls also throw the usual .NET exceptions for caller mistakes: ArgumentNullException/ArgumentOutOfRangeException (null parameters, negative timeouts), ArgumentException (a connection string the connection cannot accept, such as a StartAsync URL that is not an absolute ws/wss URI), ObjectDisposedException (using a disposed driver), InvalidOperationException (registering a module, event, or resolver after StartAsync), and OperationCanceledException (a canceled CancellationToken). StopAsync throws AggregateException when errors were accumulated under TransportErrorBehavior.Collect, and the next command throws AggregateException under Terminate when more than one error accumulated (see Transport Error Behavior Configuration).
Catching each type:
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiCommandException ex)
{
// The browser answered the command with an error response.
Console.WriteLine($"Command failed: {ex.ErrorCode} ({ex.ProtocolErrorType}): {ex.ProtocolErrorMessage}");
if (ex.RemoteStackTrace is not null)
{
Console.WriteLine(ex.RemoteStackTrace);
}
}
catch (WebDriverBiDiTimeoutException ex)
{
// No response within the command timeout (or the connection did not open in time).
Console.WriteLine($"Timed out: {ex.Message}");
}
catch (WebDriverBiDiConnectionException ex)
{
// Not connected, already connected, or the connection dropped mid-command.
Console.WriteLine($"Connection problem: {ex.Message}");
}
catch (WebDriverBiDiSerializationException ex)
{
// Parameters could not be serialized, or the response could not be deserialized.
Console.WriteLine($"Serialization problem: {ex.Message}");
}
catch (WebDriverBiDiException ex)
{
// Everything else the library raises: canceled command, unexpected result type,
// RemoteValue conversion failures, and so on.
Console.WriteLine($"Other BiDi error: {ex.Message}");
}
WebDriverBiDiException
Catching the base type handles every library failure in one place:
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"WebDriver BiDi error: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
Common Error Scenarios
Use ErrorCode on WebDriverBiDiCommandException to distinguish the error responses you expect to handle:
try
{
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiTimeoutException ex)
{
Console.WriteLine("Navigation timeout - page took too long to load");
// Handle timeout specifically
}
catch (WebDriverBiDiCommandException ex) when (ex.ErrorCode == ErrorCode.NoSuchFrame)
{
Console.WriteLine("Browsing context no longer exists");
// Handle missing context
}
catch (WebDriverBiDiCommandException ex) when (ex.ErrorCode == ErrorCode.InvalidArgument)
{
Console.WriteLine($"Invalid command parameters: {ex.ProtocolErrorMessage}");
// Handle parameter error
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Other BiDi error: {ex.Message}");
// Handle general errors
}
Transport Error Behavior Configuration
WebDriverBiDi.NET allows you to configure how transport-layer errors are handled using the TransportErrorBehavior enum. This controls errors that no command call can report: exceptions in event handlers, and messages from the browser that the transport cannot process or does not recognize.
Important: Command errors always throw exceptions immediately, regardless of this setting. This behavior only affects:
- Exceptions thrown by event handlers (
EventHandlerExceptionBehavior) - Protocol errors: an error response or registered event whose payload cannot be deserialized, or an unexpected failure while processing a message (
ProtocolErrorBehavior) - Unknown messages: a message that is not valid JSON, or not a command response, error response or registered event (
UnknownMessageBehavior) - Unexpected error responses without matching commands (
UnexpectedErrorBehavior)
Late responses are not errors. When a command times out, is canceled by its CancellationToken, or is
canceled directly through Transport.CancelCommand, the browser does not know that you stopped waiting and
may still answer. The transport remembers the commands among its most recent cancellations (the last
1,024 per session by default, adjustable through driver.TransportConfiguration.MaxTrackedCanceledCommands)
and, when such a response or error response arrives, discards it after
logging a Debug-level message through OnLogMessage and emitting the CanceledCommandResponseDiscarded
EventSource event. It is not counted under UnknownMessageBehavior or UnexpectedErrorBehavior, so
a slow navigation that times out and then completes does not terminate the session in Terminate mode.
Only a response whose command ID was never issued, or whose command has been forgotten because a full window of
further commands was canceled after it, is treated as an unknown message or unexpected error. The window
counts cancellations, so a command can be forgotten even when the late responses for the commands canceled
after it have already arrived. The commands remembered are those of the current session, so a response to a
command of an earlier session that the connection delivers only after the transport has reconnected is also
treated as an unknown message or unexpected error; command IDs are never reused, so it cannot be mistaken
for the response to a command of the new session.
Each remembered entry is a CanceledCommandInfo, carrying the command's CommandId and CommandName, the
ResponseType the answer would have been deserialized to, the TimeSinceCancellation, and a Reason of type
CommandCancellationReason:
| Reason | Meaning |
|---|---|
Canceled |
The command's CancellationToken fired, or the command was canceled directly through Transport.CancelCommand |
TimedOut |
The command's timeout elapsed before a response arrived |
ConnectionClosed |
The command was still pending when the connection closed |
The reason appears in the Debug log message and in the CanceledCommandResponseDiscarded EventSource
event's payload, which is how you tell a slow-but-successful command apart from one abandoned at shutdown.
public enum TransportErrorBehavior
{
Ignore, // Neither collect nor throw; still reported through diagnostics (default)
Collect, // Store errors for later inspection
Terminate // Throw exception on next command
}
Understanding Event Handler Error Propagation
Event handlers run on separate threads from your main application code. This means exceptions in event handlers don't directly propagate to the calling code. The transport error behavior determines what happens when event handler exceptions occur:
- Ignore (default): Exception is neither collected nor thrown; it is raised on
OnEventHandlerErrorOccurred - Collect: Exception is stored in a list for later inspection
- Terminate: Exception is stored and thrown when you send the next command
For handlers registered with ObservableEventHandlerOptions.RunHandlerAsynchronously, this behavior also applies to exceptions that occur after the handler has returned control to the transport thread. In other words, exceptions from handlers being run asynchronously are not silently dropped.
Each failing observer is reported on its own, whether it runs synchronously or asynchronously, and the report identifies it: OnEventHandlerErrorOccurred receives the observer's ObserverId and ObserverDescription, the name of the event it was added to, and the exception it threw. When several observers of one event fail, each is reported separately, and every observer is still notified.
The one important exception is capture session task capture. If you capture async handler tasks by using WaitForCapturedTasksAsync() or WaitForCapturedTasksCompleteAsync(), those task exceptions remain owned by your code. They are propagated through the captured task path rather than being surfaced again through EventHandlerExceptionBehavior.
Why Ignore is the Default
WebDriverBiDi.NET defaults all error behaviors to Ignore for several important reasons:
1. Protocol Stability During Evolution
- The WebDriver BiDi protocol is actively evolving with new features being added regularly
- Browsers may send events or messages that aren't yet fully specified
- Unknown messages (ones that match no structure the library recognizes) are common during protocol transitions
Ignoremode allows automation to continue working even when protocols diverge slightly between library and browser versions
2. Event Handler Resilience
- Event handlers are secondary to the main automation flow
- In many cases, a failing log observer or network monitor shouldn't halt critical automation workflows
- Users can explicitly handle errors within their handlers using try-catch blocks
- Forcing all users to handle potential event handler exceptions would add significant boilerplate
3. Graceful Degradation
- Libraries like this are often used for web scraping, testing, and monitoring where partial success is acceptable
- Stopping the entire driver on a malformed protocol message would be overly aggressive
- Users can opt-in to stricter error handling (Terminate mode) when needed
4. Backward Compatibility
- As browsers implement new WebDriver BiDi features, older library versions will encounter unknown protocol extensions
Ignoreallows older library versions to continue functioning with newer browsers- This is especially important for long-running automation infrastructure
When to Change from the Default:
- Development/Testing: Use
Terminatemode to catch issues early and ensure proper error handling - Diagnostics: Use
Collectmode to gather all errors for troubleshooting - Production (with mature code): Consider
Terminatemode once your automation is stable and tested
Ignore Mode (Default)
Ignore mode neither collects nor throws transport errors. This is the default behavior for all error types. An ignored error is not silent, though: each kind is still reported through the driver's diagnostic channels, so you can watch for it without changing the behavior:
| Error | Still reported through |
|---|---|
| Event handler exception | OnEventHandlerErrorOccurred, and the EventHandlerError EventSource event |
| Protocol error | OnLogMessage at Error, and, for a payload that cannot be deserialized, the ProtocolError EventSource event as well. A fault of the message-processing loop itself raises that EventSource event and is not logged. No observable event is raised for any of them |
| Unknown message | OnUnknownMessageReceived, and the UnknownMessageReceived EventSource event; a message that is not valid JSON is also written to OnLogMessage at Error |
| Unexpected error | OnUnexpectedErrorReceived |
// Default behavior - errors are ignored
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Event handler errors are neither collected nor thrown; they are raised on OnEventHandlerErrorOccurred
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// This runs on a separate thread
// If it throws, the exception is reported through OnEventHandlerErrorOccurred
ProcessLogEntry(e); // May throw
});
await driver.Session.SubscribeAsync(subscribeParams);
// Commands proceed normally; event handler errors surface only through OnEventHandlerErrorOccurred
await driver.BrowsingContext.NavigateAsync(navParams);
}
finally
{
await driver.StopAsync();
}
Use Ignore Mode When:
- Working with evolving protocol versions where unknown messages are expected
- Event handler failures shouldn't stop critical automation workflows
- You handle all errors within event handlers themselves using try-catch
- Operating in scenarios where graceful degradation is preferred
- Backward compatibility is more important than strict error reporting
⚠️ Note: While this is the default, consider using Terminate mode during development to catch issues early. The default exists primarily for protocol stability and backward compatibility, not because it's always the best choice for your use case
Collect Mode
Collect mode stores transport errors in a list, throwing them when the driver is stopped:
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection)
{
EventHandlerExceptionBehavior = TransportErrorBehavior.Collect,
ProtocolErrorBehavior = TransportErrorBehavior.Collect,
UnknownMessageBehavior = TransportErrorBehavior.Collect,
UnexpectedErrorBehavior = TransportErrorBehavior.Collect,
};
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Subscribe to events with potentially failing handlers
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// This runs on a separate thread
// If it throws, error is collected but never thrown
ProcessLogEntry(e);
});
driver.Network.OnBeforeRequestSent.AddObserver((e) =>
{
// If this throws, error is collected
ProcessNetworkRequest(e);
});
await driver.Session.SubscribeAsync(subscribeParams);
// Send commands - event handler errors won't be thrown
await driver.BrowsingContext.NavigateAsync(navParams);
await driver.Script.EvaluateAsync(evalParams);
await driver.StopAsync();
}
catch (AggregateException ex)
{
// Explicitly check for collected errors when ready
if (ex.InnerExceptions.Count > 0)
{
Console.WriteLine($"\nCollected {ex.InnerExceptions.Count} transport errors:");
foreach (Exception error in ex.InnerExceptions)
{
Console.WriteLine($" [{error.GetType().Name}] {error.Message}");
Console.WriteLine($" From: {error.StackTrace?.Split('\n')[0].Trim()}");
}
// Analyze error types
var eventHandlerErrors = ex.InnerExceptions
.Where(e => e.StackTrace?.Contains("AddObserver") == true)
.ToList();
// WebDriverBiDiProtocolException is raised for error responses that match no
// pending command (UnexpectedErrorBehavior); deserialization failures are
// collected as WebDriverBiDiSerializationException.
var protocolErrors = ex.InnerExceptions
.OfType<WebDriverBiDiProtocolException>()
.ToList();
Console.WriteLine($" Event handler errors: {eventHandlerErrors.Count}");
Console.WriteLine($" Unexpected error responses: {protocolErrors.Count}");
}
}
finally
{
await driver.DisposeAsync();
}
Use Collect Mode When:
- Diagnosing flaky or failing event handlers
- You want to continue operation despite event handler errors
- Testing error resilience of your event handling code
- You need a complete error report after operations
- Protocol errors might be transient
Important: Your code continues normally—errors throw when driver stopped as an AggregateException.
This includes exceptions from handlers being run asynchronously unless you have explicitly captured those handler tasks via a capture session.
Collected errors are thrown only by StopAsync(). DisposeAsync() calls StopAsync() internally, but it catches
the resulting AggregateException, logs it at Warn level through OnLogMessage, and does not rethrow. If you rely on
await using (or a bare DisposeAsync()) without calling StopAsync() first, every collected error is discarded after that single log message.
Always call await driver.StopAsync() inside a try block and observe the AggregateException there, as the sample above
does; the BIDI012 analyzer reports a warning when a Collect behavior is configured
in a method that disposes the driver without stopping it first.
Errors belong to the session they arose in. An error that arises while the transport processes a message received
in one session, including a fault in a handler run for that message, even one run asynchronously that faults much
later, is collected by that session alone. If the transport has reconnected by the time the error arises, that session
has ended: its collected errors can no longer be thrown by stopping it, and it can no longer be terminated. The error is
therefore not collected by the new session, under Collect or Terminate; it is logged at Warn level through
OnLogMessage instead, so it cannot fail or terminate a session it has nothing to do with.
Terminate Mode
Terminate mode stores exceptions from event handlers and throws them when you send the next command:
// Create transport with Terminate behavior (opt-in to stricter error handling)
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection)
{
EventHandlerExceptionBehavior = TransportErrorBehavior.Terminate,
ProtocolErrorBehavior = TransportErrorBehavior.Terminate,
UnknownMessageBehavior = TransportErrorBehavior.Terminate,
UnexpectedErrorBehavior = TransportErrorBehavior.Terminate,
};
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Subscribe to events
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// This runs on a separate thread
if (e.Level == LogLevel.Error)
{
throw new InvalidOperationException("Error log entry received");
}
});
await driver.Session.SubscribeAsync(subscribeParams);
// If an error log event occurs, the exception won't throw immediately
// because the event handler runs on a separate thread
// The exception will be thrown here when we send the next command. With exactly
// one accumulated error it is a WebDriverBiDiException wrapping it; with more than
// one, an AggregateException is thrown instead.
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiException ex)
{
// One accumulated error arrives here, wrapped.
Console.WriteLine($"Transport error: {ex.Message}");
}
catch (AggregateException ex)
{
// Several accumulated errors arrive here, one per inner exception.
foreach (Exception inner in ex.InnerExceptions)
{
Console.WriteLine($"Transport error: {inner.Message}");
}
}
finally
{
await driver.StopAsync();
}
Note: If an error log event occurs, the exception won't throw immediately because the event handler runs on a separate thread. The exception will be thrown when you send the next command (e.g., NavigateAsync), and your catch block will receive it. With exactly one accumulated error the thrown exception is a WebDriverBiDiException wrapping it; if more than one error accumulated before the next command, an AggregateException containing all of them is thrown instead, so catch both.
Why This Matters:
- Event handlers execute asynchronously on the transport thread
- Your main code doesn't directly wait for event handlers to complete
- Terminate mode ensures errors are eventually reported to your code, including exceptions from handlers being run asynchronously
- The error surfaces when you send the next command, which is a natural synchronization point
Capture-session-owned exceptions are different: if you use WaitForCapturedTasksAsync() or WaitForCapturedTasksCompleteAsync() to take ownership of async handler tasks, exceptions from those tasks propagate through the returned task path instead of terminating on the next command.
Use Terminate Mode When:
- You want event handler errors to be reported (recommended for development)
- You need fast failure on protocol errors
- Operating in production with known stable protocol versions
- You prefer explicit error handling over silent failures
Behavior Comparison
| Mode | Event Handler Exceptions | Protocol Errors | Command Errors | When Error Surfaces |
|---|---|---|---|---|
| Ignore (default) | Discarded and logged, including exceptions from asynchronously run handlers when those tasks are not capture-session-owned | Discarded and logged | Always throws immediately | Never |
| Collect | Stored in list, including exceptions from asynchronously run handlers when those tasks are not capture-session-owned | Stored in list | Always throws immediately | When driver stopped |
| Terminate | Throws on next command, including exceptions from asynchronously run handlers when those tasks are not capture-session-owned | Throws on next command | Always throws immediately | Synchronization point (next command); WebDriverBiDiException for one error, AggregateException for several |
When async handler tasks are explicitly owned via a capture session, their exceptions are owned by the caller instead of being routed through the transport behavior above.
Threading Model and Error Propagation
Understanding the threading model is crucial for error handling:
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// Terminate mode is what surfaces a handler's exception on a later command; under the default
// Ignore the command below completes and the catch never runs.
driver.TransportConfiguration.EventHandlerExceptionBehavior = TransportErrorBehavior.Terminate;
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Main thread (your code)
Console.WriteLine("Main thread: Setting up event handler");
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// The reader task, not the thread that started the driver
Console.WriteLine($"Reader task: Processing event {e.Type}");
// If this throws, the exception occurs on the reader task
if (e.Level == LogLevel.Error)
{
throw new InvalidOperationException("Error log entry");
}
});
await driver.Session.SubscribeAsync(subscribeParams);
Console.WriteLine("Main thread: Executing command");
try
{
// Your own thread: with Terminate mode, a handler's exception from the reader task is
// surfaced here, on the next command
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (AggregateException ex)
{
// More than one error accumulated before this command: each is an inner exception.
Console.WriteLine($"Caught errors from the reader task: {string.Join(", ", ex.InnerExceptions.Select(inner => inner.Message))}");
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Caught exception from the reader task: {ex.Message}");
}
Connection-Level Error Monitoring
For real-time error visibility without affecting behavior, use connection events:
WebSocketConnection connection = new WebSocketConnection();
// These run on the transport thread but don't throw to main thread
connection.OnConnectionError.AddObserver((errorArgs) =>
{
Console.WriteLine($"[Connection Error] {errorArgs.Exception.Message}");
LogToFile($"Transport error: {errorArgs.Exception}");
});
connection.OnLogMessage.AddObserver((logArgs) =>
{
if (logArgs.Level == WebDriverBiDiLogLevel.Error)
{
Console.WriteLine($"[Transport Error Log] {logArgs.Message}");
}
});
Transport transport = new Transport(connection);
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
Complete Error Handling Pattern
Combine all approaches for comprehensive error management. See the Collect Mode and Complete Event Handler Pattern sections for the key patterns.
Best Practices
- Use Terminate mode during development: Surfaces handler bugs and protocol issues as exceptions instead of leaving them to diagnostic events nothing may be watching
- Handle errors inside event handlers: Use try-catch within handlers when possible
- Use Collect mode for diagnostics: Helpful for troubleshooting event handler issues
- Monitor connection events: Use
driver.OnConnectionLostto learn when the connection ends, andOnConnectionErrorfor real-time error visibility - Remember the threading model: Event handlers run on separate threads
- Check for errors at logical points: With Collect mode, inspect errors after operations
- Don't lean on Ignore mode to hide errors: The Ignore default is appropriate for production only when your handlers do their own error handling (see the mode table below); it is not a substitute for explicit error handling
Script Execution Errors
Handling JavaScript Exceptions
JavaScript errors are returned as EvaluateResultException rather than thrown:
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('.missing').textContent",
new ContextTarget(contextId),
true));
if (result is EvaluateResultSuccess success)
{
string text = success.Result.As<StringRemoteValue>().Value;
Console.WriteLine($"Text: {text}");
}
else if (result is EvaluateResultException exception)
{
Console.WriteLine($"JavaScript error: {exception.ExceptionDetails.Text}");
Console.WriteLine($"Line: {exception.ExceptionDetails.LineNumber}");
Console.WriteLine($"Column: {exception.ExceptionDetails.ColumnNumber}");
// StackTrace is always present; an empty CallFrames list is how "no frames" arrives.
Console.WriteLine("Stack trace:");
foreach (var frame in exception.ExceptionDetails.StackTrace.CallFrames)
{
Console.WriteLine($" at {frame.FunctionName} ({frame.Url}:{frame.LineNumber})");
}
// Handle the error appropriately
}
Safe Script Execution Pattern
public async Task<T?> TryEvaluateAsync<T>(
BiDiDriver driver,
string expression,
string contextId,
T? defaultValue = default)
{
try
{
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
expression,
new ContextTarget(contextId),
true));
if (result is EvaluateResultSuccess success &&
success.Result is ITypeSafeRemoteValue<T> typeSafeValue)
{
return typeSafeValue.Value;
}
else if (result is EvaluateResultException exception)
{
Console.WriteLine($"Script error: {exception.ExceptionDetails.Text}");
return defaultValue;
}
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Command error: {ex.Message}");
}
return defaultValue;
}
// Usage
string? title = await TryEvaluateAsync<string>(
driver,
"document.title",
contextId,
"Unknown");
Connection Errors
Handling Disconnections
public class ResilientDriver
{
private BiDiDriver driver;
private readonly string webSocketUrl;
private readonly int maxRetries = 3;
public async Task<T> ExecuteWithRetryAsync<T>(
Func<BiDiDriver, Task<T>> operation)
{
int attempt = 0;
Exception? lastException = null;
while (attempt < maxRetries)
{
try
{
return await operation(driver);
}
catch (WebDriverBiDiException ex) when (
ex.Message.Contains("WebSocket") ||
ex.Message.Contains("connection"))
{
lastException = ex;
attempt++;
if (attempt < maxRetries)
{
Console.WriteLine($"Connection lost. Reconnecting... (attempt {attempt})");
await Task.Delay(TimeSpan.FromSeconds(2));
// Reconnect
await driver.StopAsync();
driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);
}
}
}
throw new Exception(
$"Operation failed after {maxRetries} attempts",
lastException);
}
}
// Usage
ResilientDriver resilientDriver = new ResilientDriver();
NavigateCommandResult result = await resilientDriver.ExecuteWithRetryAsync(
async (d) => await d.BrowsingContext.NavigateAsync(navParams));
Timeout Handling
For per-command timeouts, prefer the built-in timeoutOverride parameter over custom patterns (e.g., Task.WhenAny with Task.Delay). Use custom patterns only when you need different semantics than the built-in timeout.
Preferred Approach: Use the Built-in Timeout Override
The simplest and recommended way to set a per-command timeout is the timeoutOverride parameter on module methods. Every module command accepts this as the second parameter:
// Per-driver default timeout (set when creating the driver)
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// Per-command timeout override (preferred)
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(
navParams,
TimeSpan.FromSeconds(60)); // Override for this command only
// Quick operations can use a shorter timeout
StatusCommandResult status = await driver.Session.StatusAsync(
null,
timeoutOverride: TimeSpan.FromSeconds(5));
You can also use ExecuteCommandAsync when working at the driver level:
NavigateCommandResult result = await driver.ExecuteCommandAsync<NavigateCommandResult>(
navParams,
TimeSpan.FromSeconds(60));
When to Use Custom Timeout Patterns
Use a custom pattern (e.g., Task.WhenAny with Task.Delay) only when you need different semantics than the built-in timeout—for example, returning null instead of throwing, or implementing custom retry logic:
public async Task<NavigateCommandResult?> NavigateWithTimeoutAsync(
BiDiDriver driver,
NavigateCommandParameters parameters,
TimeSpan timeout)
{
Task<NavigateCommandResult> navigationTask =
driver.BrowsingContext.NavigateAsync(parameters, timeout);
try
{
return await navigationTask;
}
catch (WebDriverBiDiTimeoutException)
{
Console.WriteLine($"Navigation timeout after {timeout.TotalSeconds}s");
return null;
}
}
// Usage
NavigateCommandResult? result = await NavigateWithTimeoutAsync(
driver,
navParams,
TimeSpan.FromSeconds(30));
if (result == null)
{
Console.WriteLine("Navigation failed - timeout");
// Handle timeout
}
When to use each:
| Need | Use |
|---|---|
| Standard per-command timeout | timeoutOverride parameter on module methods |
Return null on timeout instead of throwing |
Custom wrapper that catches WebDriverBiDiTimeoutException |
| Different timeout per retry attempt | Custom retry loop with timeoutOverride |
Element Not Found Handling
Safe Element Location
public async Task<RemoteValue?> FindElementSafelyAsync(
BiDiDriver driver,
string contextId,
string selector,
TimeSpan timeout)
{
DateTime endTime = DateTime.Now + timeout;
while (DateTime.Now < endTime)
{
try
{
LocateNodesCommandResult result =
await driver.BrowsingContext.LocateNodesAsync(
new LocateNodesCommandParameters(contextId, new CssLocator(selector)));
if (result.Nodes.Count > 0)
{
return result.Nodes[0];
}
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Error locating element: {ex.Message}");
}
await Task.Delay(100);
}
Console.WriteLine($"Element not found after {timeout.TotalSeconds}s: {selector}");
return null;
}
// Usage
RemoteValue? element = await FindElementSafelyAsync(
driver,
contextId,
"button.submit",
TimeSpan.FromSeconds(10));
if (element == null)
{
Console.WriteLine("Submit button not found");
// Handle missing element
}
Wait for Element Pattern
public async Task<bool> WaitForElementAsync(
BiDiDriver driver,
string contextId,
string selector,
TimeSpan timeout)
{
string waitScript = $$"""
new Promise((resolve) => {
const checkElement = () => {
const element = document.querySelector('{{selector}}');
if (element) {
resolve(true);
} else {
setTimeout(checkElement, 100);
}
};
checkElement();
setTimeout(() => resolve(false), {{timeout.TotalMilliseconds}});
})
""";
try
{
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
waitScript,
new ContextTarget(contextId),
true));
if (result is EvaluateResultSuccess success &&
success.Result is BooleanRemoteValue boolValue)
{
return boolValue.Value;
}
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Error waiting for element: {ex.Message}");
}
return false;
}
Event Handler Errors
Observable Event Handler Options
Event handlers can be configured with ObservableEventHandlerOptions to control execution behavior:
// The default: the reader task waits for the handler, so the next event is not dispatched until it
// returns, and a command sent from here cannot be answered while it runs.
driver.Log.OnEntryAdded.AddObserver(
(e) => Console.WriteLine($"Log entry: {e.Text}"),
ObservableEventHandlerOptions.RunHandlerSynchronously);
// Queued to the thread pool: the reader task moves on at once, so a handler that performs I/O, or
// drives the driver, does not hold up the events behind it.
driver.Log.OnEntryAdded.AddObserver(
async (e) => await ArchiveLogEntryAsync(e),
ObservableEventHandlerOptions.RunHandlerAsynchronously);
public enum ObservableEventHandlerOptions
{
RunHandlerSynchronously = 0, // Synchronous execution (default)
RunHandlerAsynchronously = 1 // Asynchronous execution
}
Synchronous vs. Asynchronous Handlers
By default (RunHandlerSynchronously), event handlers run synchronously on the transport thread, which blocks message processing:
// ❌ Bad: Synchronous handler blocks transport thread
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// This runs on the transport thread, blocking it
Thread.Sleep(1000); // Blocks ALL message processing for 1 second!
ProcessLogEntry(e);
});
// ❌ Also bad: Default (RunHandlerSynchronously) still blocks
driver.Log.OnEntryAdded.AddObserver((e) =>
{
ProcessLogEntry(e); // Blocks transport thread until complete
}, ObservableEventHandlerOptions.RunHandlerSynchronously);
// ✅ Good: Asynchronous handler doesn't block transport
driver.Log.OnEntryAdded.AddObserver(
async (e) =>
{
// This runs on a Task pool thread
await Task.Delay(1000); // Doesn't block transport thread
await ProcessLogEntryAsync(e);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
When to Use Asynchronous Handlers:
- Handler performs I/O operations (file, network, database)
- Handler does CPU-intensive work
- Handler calls async APIs
- You want to avoid blocking message processing
- Recommended for most scenarios where handler does more than trivial work
Exception Handling in Event Handlers
Event handler exceptions are subject to the TransportErrorBehavior setting. Unhandled exceptions depend on the mode (Terminate/Collect/Ignore). Handle exceptions within the handler for best results:
// Unhandled exception - behavior depends on TransportErrorBehavior
driver.Log.OnEntryAdded.AddObserver((e) =>
{
ProcessLogEntry(e); // May throw
// If throws:
// Terminate mode: Exception thrown on next command
// Collect mode: Exception stored by the transport and thrown from StopAsync()
// Ignore mode: Exception discarded
});
// ✅ Better: Handle exceptions within the handler
driver.Log.OnEntryAdded.AddObserver((e) =>
{
try
{
ProcessLogEntry(e);
}
catch (Exception ex)
{
Console.WriteLine($"Error processing log entry: {ex.Message}");
// Error handled, won't be reported to TransportErrorBehavior
}
});
// ✅ Best: Async handler with error handling
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
try
{
await ProcessNetworkRequestAsync(e);
}
catch (Exception ex)
{
await LogErrorAsync($"Error processing request: {ex.Message}");
// Error handled within handler
}
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
Handler Execution Behavior
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Synchronous handler (default)
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// Runs on transport thread
// Blocks transport until complete
// Other messages wait for this to finish
ProcessLogEntry(e);
});
// Explicit synchronous
driver.Log.OnEntryAdded.AddObserver((e) =>
{
ProcessLogEntry(e);
}, ObservableEventHandlerOptions.RunHandlerSynchronously);
// Asynchronous handler
driver.Network.OnResponseCompleted.AddObserver(
async (e) =>
{
// Starts on transport thread, continues on task pool
// Transport thread returns immediately
// Other messages processed concurrently
await AnalyzeResponseAsync(e);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
Multiple Handlers for Same Event
When multiple handlers are registered, they all execute in sequence. With synchronous handlers, Handler 1 executes completely, then Handler 2. With asynchronous handlers, both start and run concurrently:
// Handler 1 (synchronous)
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine($"Handler 1: {e.Text}");
});
// Handler 2 (synchronous)
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine($"Handler 2: {e.Text}");
});
// With synchronous handlers:
// - Handler 1 executes completely, then Handler 2 executes
// - If Handler 1 throws, TransportErrorBehavior determines what happens
// - With Terminate: Exception thrown on next command
// - With Collect: Exception collected, Handler 2 still executes
// - With Ignore: Exception ignored, Handler 2 still executes
// Handler 1 (asynchronous)
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await LogRequestAsync(e.Request.Url);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
// Handler 2 (asynchronous)
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await AnalyzeSecurityHeadersAsync(e.Request);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
// With asynchronous handlers:
// - Both handlers start and run concurrently
// - Transport thread doesn't wait for either to complete
// - Exceptions handled according to TransportErrorBehavior
Complete Event Handler Pattern
// Use Collect mode to see all handler errors
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection)
{
EventHandlerExceptionBehavior = TransportErrorBehavior.Collect,
ProtocolErrorBehavior = TransportErrorBehavior.Collect,
UnknownMessageBehavior = TransportErrorBehavior.Collect,
UnexpectedErrorBehavior = TransportErrorBehavior.Collect,
};
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
try
{
// Synchronous handler for quick operations
driver.Log.OnEntryAdded.AddObserver((e) =>
{
try
{
// Quick, synchronous operation
if (e.Level == LogLevel.Error || e.Level == LogLevel.Warn)
{
Console.WriteLine($"[{e.Level}] {e.Text}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in log handler: {ex.Message}");
}
});
// Asynchronous handler for I/O operations
driver.Network.OnResponseCompleted.AddObserver(
async (e) =>
{
try
{
// Async operation doesn't block transport
await SaveResponseToFileAsync(e.Response);
}
catch (Exception ex)
{
await LogErrorAsync($"Failed to save response: {ex.Message}");
}
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
await driver.Session.SubscribeAsync(subscribeParams);
// ... perform the automation ...
// Collected errors are thrown only by StopAsync; DisposeAsync alone would
// log and discard them, so stop explicitly before the finally disposes.
await driver.StopAsync();
}
catch (AggregateException ex)
{
// After operations, check for any unhandled handler errors
if (ex.InnerExceptions.Count > 0)
{
Console.WriteLine($"Transport errors occurred: {ex.InnerExceptions.Count}");
foreach (var error in ex.InnerExceptions)
{
Console.WriteLine($" - {error.Message}");
}
}
}
finally
{
await driver.DisposeAsync();
}
Handler Options Decision Guide
| Handler Does | Use Option | Why |
|---|---|---|
| Quick in-memory work (<10ms) | RunHandlerSynchronously (default) | Minimal overhead, acceptable blocking |
| File I/O | RunHandlerAsynchronously | Don't block transport on disk operations |
| Network requests | RunHandlerAsynchronously | Don't block transport on network |
| Database queries | RunHandlerAsynchronously | Don't block transport on DB operations |
| CPU-intensive work | RunHandlerAsynchronously | Don't block transport thread |
| Logging to console | RunHandlerSynchronously | Quick operation, synchronous is fine |
| Updating counters/state | RunHandlerSynchronously | Quick operation, synchronous is fine |
Best Practices for Event Handlers
- Use async handlers for I/O: Prevent blocking the transport thread
- Handle exceptions internally: Use try-catch within handlers when possible
- Keep handlers fast: Even async handlers should complete quickly
- Don't perform long operations: Offload heavy work to background services
- Test handler error paths: Verify your error handling works correctly
- Monitor handler performance: Track execution time to identify bottlenecks
Validation and Defensive Programming
Parameter Validation
public async Task<NavigateCommandResult> SafeNavigateAsync(
BiDiDriver driver,
string contextId,
string url)
{
// Validate inputs
if (driver == null)
{
throw new ArgumentNullException(nameof(driver));
}
if (string.IsNullOrWhiteSpace(contextId))
{
throw new ArgumentException("Context ID cannot be empty", nameof(contextId));
}
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentException("URL cannot be empty", nameof(url));
}
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri))
{
throw new ArgumentException("Invalid URL format", nameof(url));
}
// Execute with error handling
try
{
NavigateCommandParameters navParams = new NavigateCommandParameters(contextId, url)
{
Wait = ReadinessState.Complete
};
return await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiException ex)
{
throw new InvalidOperationException(
$"Failed to navigate to {url}: {ex.Message}",
ex);
}
}
Context Validation
public async Task<bool> IsContextValidAsync(BiDiDriver driver, string contextId)
{
try
{
// Use RootBrowsingContextId to fetch only this context (more efficient than full tree)
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters { RootBrowsingContextId = contextId });
return tree.ContextTree.Count > 0;
}
catch (WebDriverBiDiException)
{
return false;
}
}
// Usage
if (!await IsContextValidAsync(driver, contextId))
{
Console.WriteLine("Context no longer exists");
// Handle invalid context
}
Logging and Diagnostics
Comprehensive Logging
public class DiagnosticDriver
{
private readonly BiDiDriver driver;
private readonly ILogger logger;
public async Task<T> ExecuteWithLoggingAsync<T>(
string operationName,
Func<Task<T>> operation)
{
logger.LogInformation($"Starting {operationName}");
DateTime startTime = DateTime.Now;
try
{
T result = await operation();
TimeSpan duration = DateTime.Now - startTime;
logger.LogInformation(
$"Completed {operationName} in {duration.TotalMilliseconds}ms");
return result;
}
catch (WebDriverBiDiException ex)
{
TimeSpan duration = DateTime.Now - startTime;
logger.LogError(
ex,
$"Failed {operationName} after {duration.TotalMilliseconds}ms: {ex.Message}");
throw;
}
}
}
Error Context Capture
public class ErrorContext
{
public string Operation { get; set; }
public DateTime Timestamp { get; set; }
public string? ContextId { get; set; }
public string? Url { get; set; }
public Exception Exception { get; set; }
public void SaveToFile()
{
string fileName = $"error-{Timestamp:yyyyMMdd-HHmmss}.log";
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Operation: {Operation}");
sb.AppendLine($"Timestamp: {Timestamp:yyyy-MM-dd HH:mm:ss.fff}");
sb.AppendLine($"Context ID: {ContextId ?? "N/A"}");
sb.AppendLine($"URL: {Url ?? "N/A"}");
sb.AppendLine($"\nException: {Exception.Message}");
sb.AppendLine($"\nStack Trace:\n{Exception.StackTrace}");
if (Exception.InnerException != null)
{
sb.AppendLine($"\nInner Exception: {Exception.InnerException.Message}");
}
File.WriteAllText(fileName, sb.ToString());
}
}
// Usage
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (Exception ex)
{
ErrorContext context = new ErrorContext
{
Operation = "Navigation",
Timestamp = DateTime.Now,
ContextId = contextId,
Url = navParams.Url,
Exception = ex
};
context.SaveToFile();
throw;
}
Recovery Strategies
Graceful Degradation
public async Task<string> GetPageTitleAsync(BiDiDriver driver, string contextId)
{
// Try JavaScript first
try
{
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.title",
new ContextTarget(contextId),
true));
if (result is EvaluateResultSuccess success &&
success.Result is StringRemoteValue stringValue)
{
return stringValue.Value ?? "Unknown";
}
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"JavaScript method failed: {ex.Message}");
}
// Fallback to context info
try
{
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters { RootBrowsingContextId = contextId });
if (tree.ContextTree.Count > 0)
{
return tree.ContextTree[0].Url;
}
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Context method failed: {ex.Message}");
}
return "Unknown";
}
Circuit Breaker Pattern
public class CircuitBreaker
{
private int failureCount = 0;
private readonly int threshold = 5;
private readonly TimeSpan resetTimeout = TimeSpan.FromMinutes(1);
private DateTime? openedAt = null;
private bool isOpen = false;
public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)
{
if (isOpen)
{
if (openedAt.HasValue &&
DateTime.Now - openedAt.Value > resetTimeout)
{
// Try to reset
isOpen = false;
failureCount = 0;
openedAt = null;
}
else
{
throw new InvalidOperationException(
"Circuit breaker is open - too many failures");
}
}
try
{
T result = await operation();
// Success - reset counter
if (failureCount > 0)
{
failureCount = 0;
}
return result;
}
catch (Exception)
{
failureCount++;
if (failureCount >= threshold)
{
isOpen = true;
openedAt = DateTime.Now;
}
throw;
}
}
}
// Usage
CircuitBreaker breaker = new CircuitBreaker();
try
{
NavigateCommandResult result = await breaker.ExecuteAsync(
async () => await driver.BrowsingContext.NavigateAsync(navParams));
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Circuit breaker"))
{
Console.WriteLine("Too many failures - circuit breaker activated");
// Handle circuit breaker state
}
Testing Error Scenarios
Simulating Errors
[Test]
public async Task TestNavigationTimeout()
{
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(1));
try
{
await driver.StartAsync(webSocketUrl);
// This should timeout
NavigateCommandParameters parameters = new NavigateCommandParameters(
contextId,
"https://httpstat.us/200?sleep=5000"); // 5 second delay
await driver.BrowsingContext.NavigateAsync(parameters);
Assert.Fail("Expected timeout exception");
}
catch (WebDriverBiDiTimeoutException ex)
{
Assert.That(ex.Message, Does.Contain("Timed out"));
}
}
[Test]
public async Task TestInvalidContext()
{
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(1));
try
{
await driver.StartAsync(webSocketUrl);
// Use invalid context ID
NavigateCommandParameters parameters = new NavigateCommandParameters(
"invalid-context-id",
"https://example.com");
await driver.BrowsingContext.NavigateAsync(parameters);
Assert.Fail("Expected context error");
}
catch (WebDriverBiDiException ex)
{
Assert.That(ex.Message, Does.Contain("no such frame").IgnoreCase);
}
}
Best Practices
- Always handle WebDriverBiDiException: Protocol errors should be caught and handled
- Check script result types: Don't assume success, check for exceptions
- Validate inputs: Check parameters before sending commands
- Log errors comprehensively: Include context for debugging
- Implement retries: Handle transient failures with retry logic
- Use timeouts: Always specify appropriate timeouts
- Clean up resources: Use try-finally or using statements
- Test error paths: Write tests for failure scenarios
- Provide fallbacks: Implement graceful degradation where possible
- Monitor health: Track error rates and patterns
Common Pitfalls
Don't Swallow Exceptions
// ❌ Bad: Silent failure
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch
{
// Error ignored
}
// ✅ Good: Proper handling
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiException ex)
{
logger.LogError(ex, "Navigation failed");
throw; // or handle appropriately
}
Don't Use Generic Catch
// ❌ Bad: Too broad
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (Exception ex)
{
// Catches everything, including bugs
}
// ✅ Good: Specific handling
try
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiException ex)
{
// Handle protocol errors
}
catch (TaskCanceledException ex)
{
// Handle cancellation
}
Troubleshooting and FAQ
Common Error Messages
| Error Message | Likely Cause | What to Do |
|---|---|---|
| "Transport must be connected to a remote end to execute commands" | Commands sent before StartAsync or after disconnect |
Ensure StartAsync has completed before sending commands. Check IsStarted before operations. |
| "no such frame" / "no such window" | Browsing context was closed or no longer exists | Verify the context ID is still valid. Use GetTreeAsync to refresh context list. |
| "Timed out executing command" | Command exceeded the timeout | Increase timeoutOverride for the command, or construct the driver with a larger DefaultCommandTimeout (it is set via the BiDiDriver constructor). |
| "Cannot register a type info resolver after the transport is connected" | RegisterTypeInfoResolverAsync called after StartAsync |
Register type resolvers before calling StartAsync. |
| "This observable event only allows N observer(s)" | Too many observers added to an event with MaxObserverCount |
Remove observers with Unobserve() or Dispose() before adding new ones. |
"This observable event only allows 1 observer" on OnDataReceived |
OnDataReceived transfers ownership of a pooled buffer, so it admits only the Transport |
Do not observe OnDataReceived. To inspect traffic, set LogLevel to Trace and observe OnLogMessage. See Connection Management. |
| "The provided connection already has a listener for its OnDataReceived event" | A Transport was constructed over a connection whose OnDataReceived was already observed |
Remove the observer; the Transport requires exclusive use of that event. |
Connection Diagnostics
When the connection fails or behaves unexpectedly:
- Verify the WebSocket URL: Ensure the URL is a WebDriver BiDi endpoint — the
webSocketUrlfrom a driver session (e.g.,ws://localhost:9515/session/...) or Firefox's/session— not Chrome's CDP/devtools/browser/...URL, which accepts the connection but fails every command. - Check browser is running: The remote end must be listening before you connect.
- Use connection events: Subscribe to
connection.OnConnectionErrorandconnection.OnLogMessagefor real-time diagnostics, and todriver.OnConnectionLostto learn when the browser has closed the connection. - Inspect
UnhandledErrors:Transport.UnhandledErrorsisprotected, so it is readable only from within your ownTransportsubclass (see Custom Modules — Pending commands and unhandled errors for its members); from application code, observe collected errors through theAggregateExceptionthrown byStopAsync()(when usingTransportErrorBehavior.Collect).
Interpreting TransportErrorBehavior
| Mode | When to Use | Trade-off |
|---|---|---|
| Ignore (default) | Protocol evolution, backward compatibility, event handlers that should not stop automation | Errors may go unnoticed. Use for production when you handle errors in handlers. |
| Collect | Diagnostics, gathering all errors for post-mortem analysis | Errors surface only when you stop the driver. Operation continues despite errors. |
| Terminate | Development, strict protocol conformance, fail-fast behavior | First error stops the driver on the next command. Best for catching issues early. |
Event Handlers Not Firing
If events are not received:
- Add observer before subscribing: Call
AddObserverbeforeSession.SubscribeAsync. - Subscribe to the event: Adding an observer alone is not enough—you must call
Session.SubscribeAsyncwith the event names. - Use correct event names: Prefer
driver.Log.OnEntryAdded.EventNameover string literals to avoid typos. - Check context filtering: If you passed
ContextsorUserContextstoSubscribeCommandParameters, events are limited to those contexts.
See Also
- API Design Guide: Timeout and cancellation patterns
- Architecture: Transport, connection lifecycle, error configuration
- Common Pitfalls: Event subscription, blocking handlers, registration timing
Next Steps
- Performance Considerations: Optimize your automation
- Core Concepts: Understanding the fundamentals
- Architecture: System design and patterns