Common Pitfalls
This guide covers frequently encountered issues and misunderstandings when working with WebDriverBiDi.NET. Understanding these pitfalls will help you avoid common mistakes and build more robust automation.
Event Handler Execution
Pitfall: Blocking the Transport Thread with Synchronous Handlers
The Problem:
By default, event handlers run synchronously on the transport thread, which blocks all message processing until the handler completes. This can cause serious performance issues.
// ❌ BAD: Blocks transport thread for 5 seconds
driver.Network.OnBeforeRequestSent.AddObserver((e) =>
{
Thread.Sleep(5000);
Console.WriteLine($"Request: {e.Request.Url}");
// During these 5 seconds:
// - No other events are processed
// - No command responses are received
// - Commands may timeout
// - The browser may become unresponsive
});
Why This Happens:
WebDriverBiDi.NET processes all incoming messages from the browser on a single reader task, which runs on the thread pool. When your handler blocks it, nothing else can be processed.
The Solution:
Use ObservableEventHandlerOptions.RunHandlerAsynchronously for any handler that:
- Performs I/O operations (file, network, database)
- Does CPU-intensive work
- Calls other async APIs
- Takes more than a few milliseconds
// ✅ GOOD: Runs asynchronously without blocking
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await Task.Delay(5000); // Doesn't block transport thread
Console.WriteLine($"Request: {e.Request.Url}");
// Transport thread continues processing other messages
// while the continuation after 'await' runs on a thread pool
// thread. Note: the handler is still *invoked* on the transport
// thread; the option detaches the returned Task, so put the
// await before any heavy work.
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
One Caveat: the option detaches the Task the handler returns; it does not move where the handler starts. The handler is still invoked on the transport thread, so in an async lambda the code before the first real await runs there, and a non-async handler that does its work and then returns Task.CompletedTask is not offloaded at all. Write the handler as async and await before the heavy work (an await Task.Yield(); first statement guarantees it), or wrap the work in Task.Run. Action<T> handlers are queued to the thread pool as a whole and need no such care. The BIDI007/BIDI023 analyzers flag handlers where the option cannot help, including blocking work or a module command placed before an async handler's first await.
When Synchronous is OK:
Synchronous handlers are fine for quick operations:
// ✅ Fine: Quick in-memory operation
int requestCount = 0;
driver.Network.OnBeforeRequestSent.AddObserver((e) =>
{
requestCount++;
});
// ✅ Fine: Simple logging to console
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine($"[{e.Level}] {e.Text}");
});
Key Takeaway: If your handler does anything more than updating in-memory state or simple console output, use RunHandlerAsynchronously.
Event Subscription
Pitfall: Forgetting the Two-Step Subscription Process
The Problem:
Many developers expect that adding an observer is sufficient to receive events. It's not.
// ❌ INCOMPLETE: Observer added but no events will be received
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine(e.Text);
});
await driver.BrowsingContext.NavigateAsync(navParams);
// No log events will fire - you forgot to subscribe!
Why This Design:
The two-step process (add observer + subscribe) is intentional and prevents race conditions. It ensures:
- Your handlers are in place before events start flowing
- You have explicit control over which events are subscribed
- You can scope subscriptions to specific browsing contexts
The Solution:
Always add observer first, then subscribe through the Session module:
// ✅ CORRECT: Add observer AND subscribe
// Step 1: Add observer
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine(e.Text);
});
// Step 2: Subscribe to events
SubscribeCommandParameters subscribe = new SubscribeCommandParameters(driver.Log.OnEntryAdded.EventName);
await driver.Session.SubscribeAsync(subscribe);
// Now events will be received
await driver.BrowsingContext.NavigateAsync(navParams);
Best Practice - Subscribe Multiple Events at Once:
// Add all observers first
driver.Log.OnEntryAdded.AddObserver(logHandler);
driver.Network.OnBeforeRequestSent.AddObserver(networkHandler);
driver.BrowsingContext.OnLoad.AddObserver(loadHandler);
// Then subscribe to all events in one call
SubscribeCommandParameters subscribe = new SubscribeCommandParameters(
[
driver.Log.OnEntryAdded.EventName,
driver.Network.OnBeforeRequestSent.EventName,
driver.BrowsingContext.OnLoad.EventName,
]
);
await driver.Session.SubscribeAsync(subscribe);
Key Takeaway: Adding an observer only registers your handler locally. You must explicitly subscribe through Session.SubscribeAsync() to tell the browser to send events.
Module Registration Timing
Pitfall: Registering Modules After StartAsync()
The Problem:
Attempting to register custom modules (RegisterModule), custom events (RegisterEvent), or JSON type
info resolvers (RegisterTypeInfoResolverAsync) after calling StartAsync() throws an
InvalidOperationException.
// ❌ WRONG: Registration after starting
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// This will throw InvalidOperationException!
driver.RegisterModule(new CustomModule(driver));
// Adding an observer is NOT restricted; this is allowed after StartAsync.
// Just be sure to add observers before calling Session.SubscribeAsync.
driver.Log.OnEntryAdded.AddObserver(handler);
Why This Restriction:
This timing restriction ensures:
- Every event the transport can receive has a registered deserializer before messages start flowing
- No race conditions between module registration and event dispatch
- Predictable initialization order
- Thread-safe module setup
What is not restricted: adding observers with AddObserver (or data collectors with
AddDataCollector) is allowed at any time, before or after StartAsync(), and is thread-safe with
respect to event dispatch. The ordering that matters for observers is relative to
Session.SubscribeAsync(): add the observer first so that no events are missed once the browser
starts sending them (see Event Subscription). Adding observers before
StartAsync() is simply the most convenient place to do it.
The Solution:
Register modules, custom events, and type resolvers BEFORE calling StartAsync(); add observers before
subscribing:
// ✅ CORRECT: Registration before starting
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// 1. Register custom modules (if any)
driver.RegisterModule(new CustomModule(driver));
// 2. Add event observers
driver.Log.OnEntryAdded.AddObserver((e) => Console.WriteLine(e.Text));
driver.BrowsingContext.OnLoad.AddObserver((e) => Console.WriteLine($"Loaded: {e.Url}"));
// 3. NOW start the driver
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// 4. Subscribe to events through Session module
SubscribeCommandParameters subscribe = new SubscribeCommandParameters(
[
driver.Log.OnEntryAdded.EventName,
driver.BrowsingContext.OnLoad.EventName,
]
);
await driver.Session.SubscribeAsync(subscribe);
// 5. Execute commands
await driver.BrowsingContext.NavigateAsync(navParams);
Correct Initialization Order:
1. Create BiDiDriver
2. Register custom modules (if needed)
3. Add event observers (recommended here, but permitted at any time)
4. Call StartAsync()
5. Subscribe to events via Session.SubscribeAsync()
6. Execute commands
Key Takeaway: Think of module, custom event, and type resolver registration as "configuration" that must happen before "connection" (StartAsync). Observers are not part of that restriction; just make sure they are in place before you subscribe.
Null vs Empty Collections
Pitfall: Not Knowing Which Shape an Optional List Has
The Problem:
Sent types — the CommandParameters classes and the objects nested inside them — expose optional lists in two shapes, and the way you add items differs:
- Read-only, always initialized —
List<string> Contexts { get; },UserContexts,StartNodes,Arguments,UrlPatterns,PageRanges, and every other optional list. Add items with a collection initializer or.Add(). An empty list means "not specified": the property is omitted from the payload, and an empty array is never sent (for lists the protocol requires to be non-empty, the browser would reject it; for the rest, omission and[]mean the same thing). - Nullable and settable —
HeadersandCookiesonContinueRequestCommandParameters,ContinueResponseCommandParametersandProvideResponseCommandParameters, plusBrandsandFullVersionListonClientHintsMetadata, withFormFactorsshaped like them for parity although the specification's emulation does not yet use it. Here the protocol gives a present-but-empty array its own meaning —[]replaces the headers or cookies with none, or overrides the browser's own client hint with an empty value, while omission keeps the originals — sonullomits the property and an empty list sends[].
// Shape 1: read-only, always initialized. Every optional list
// (contexts, userContexts, startNodes, arguments, urlPatterns, ...).
SetLocaleOverrideCommandParameters localeParams = new SetLocaleOverrideCommandParameters()
{
Locale = "en-US",
Contexts = { contextId }, // collection initializer - no `new List<string>()`
};
localeParams.UserContexts.Add(userContextId); // ...or add later
// Shape 2: nullable and settable. The headers and cookies of the network continueRequest,
// continueResponse and provideResponse commands, and Brands, FullVersionList and FormFactors on
// ClientHintsMetadata, where the protocol distinguishes an absent field from an empty array.
continueParams.Headers = []; // sends "headers": []
ClientHintsMetadata clientHints = new ClientHintsMetadata
{
Brands = [], // sends "brands": [], overriding the browser's own with none
FullVersionList = null, // omitted, so the browser's own is kept
};
Example Protocol Difference:
// Read-only lists: empty means "not specified" and the property is omitted
SetLocaleOverrideCommandParameters p1 = new SetLocaleOverrideCommandParameters()
{
Locale = "en-US",
};
// JSON sent: { "locale": "en-US" } (no "contexts"; the override applies everywhere)
// Read-only lists: items are sent as an array
SetLocaleOverrideCommandParameters p2 = new SetLocaleOverrideCommandParameters()
{
Locale = "en-US",
Contexts = { "<valid browsing context ID>" },
};
// JSON sent: { "locale": "en-US", "contexts": ["<valid browsing context ID>"] }
// There is no way to send "contexts": [] - the browser rejects it as an invalid
// argument, so the library does not let you express it.
// Nullable lists keep all three states
continueParams.Headers = null; // "headers" omitted: keep the original headers
continueParams.Headers = []; // "headers": [] - send the request with no headers
The Solution:
Read-only lists need no initialization. For nullable lists, initialize before adding items (analyzer BIDI017 flags a missing ??=), or assign the whole list:
// ✅ Read-only lists: nothing to initialize, just add
localeParams.Contexts.Add("<valid browsing context ID>");
// ✅ Nullable lists: initialize before adding (analyzer BIDI017 flags a missing ??=)
continueParams.Headers ??= [];
continueParams.Headers.Add(header);
// ✅ Nullable lists: or assign in one step
continueParams.Headers = [header];
Key Takeaway: Optional lists are read-only and omitted while empty. The six network Headers/Cookies lists and the three ClientHintsMetadata lists (FormFactors for parity with the other two) are the exceptions, nullable so that "omit" (null) and "send []" (empty list) stay distinguishable where the protocol tells them apart.
Command Timeouts
Pitfall: Not Understanding the Default Timeout
The Problem:
Developers are sometimes surprised that the default command timeout is 60 seconds. That is BiDiDriver.DefaultCommandWaitTimeout, which a driver uses when it is constructed without a timeout.
// Default timeout is 60 seconds!
BiDiDriver driver = new BiDiDriver();
// This command has 60 seconds to complete
await driver.BrowsingContext.NavigateAsync(navParams);
Why 60 Seconds:
The high default timeout is intentional because:
- Browser automation can involve genuinely slow operations
- Page loads can take a long time (slow networks, heavy pages)
- Script execution might be CPU-intensive
- Network requests in tests might be slow
- Better to have a long default than frequent timeouts
The Solution:
Set appropriate timeouts for your use case. Prefer the timeoutOverride parameter on module methods (e.g., NavigateAsync(parameters, TimeSpan.FromSeconds(60))) over ExecuteCommandAsync when you need per-command overrides:
// ✅ For fast operations (local testing)
BiDiDriver fastDriver = new BiDiDriver(TimeSpan.FromSeconds(10));
// ✅ For normal web automation
BiDiDriver normalDriver = new BiDiDriver(TimeSpan.FromSeconds(30));
// ✅ For slow operations or large pages
BiDiDriver slowDriver = new BiDiDriver(TimeSpan.FromMinutes(2));
// ✅ Override per-command for specific cases (preferred: use timeoutOverride on module method)
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// Most commands use 30 second timeout
// But this specific navigation gets longer timeout
await driver.BrowsingContext.NavigateAsync(
slowPageParams,
TimeSpan.FromMinutes(5));
Key Takeaway: The 60-second default is intentionally set to accommodate slow operations. Choose a timeout that matches your typical use case, and use the timeoutOverride parameter on module methods when only specific commands need a different timeout.
Event Handler Synchronization
Pitfall: Not Waiting for Async Handlers to Complete
The Problem:
When using RunHandlerAsynchronously, the handler runs on a background task. Your main code might continue before the handler finishes.
// ❌ PROBLEM: Handler might not finish before program exits
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await Task.Delay(5000); // Long-running operation
await SaveRequestToFileAsync(e.Request);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
await driver.Session.SubscribeAsync(subscribeParams);
await driver.BrowsingContext.NavigateAsync(navParams);
// Navigation completes, but handlers might still be running!
// If program exits here, handlers may not finish
Why This Happens:
NavigateAsync()completes when the browser responds to the command- Async event handlers run independently on background tasks
- There's no automatic synchronization between command completion and handler completion
The Solution - Option 1: Use WaitForCapturedTasksCompleteAsync (Recommended):
// ✅ GOOD: Use built-in helper
EventObserver<BeforeRequestSentEventArgs> observer =
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await Task.Delay(5000);
await SaveRequestToFileAsync(e.Request);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
await driver.Session.SubscribeAsync(subscribeParams);
// Start capturing events
observer.StartCapturingTasks();
// Trigger events
await driver.BrowsingContext.NavigateAsync(navParams);
// Wait for events to occur AND handlers to complete. The result says whether the expected number
// arrived: false means the timeout elapsed first, and some handlers are still running.
bool allCompleted = await observer.WaitForCapturedTasksCompleteAsync(5, TimeSpan.FromSeconds(10));
Console.WriteLine(allCompleted ? "All handlers completed" : "Timed out waiting for handlers");
observer.StopCapturingTasks();
The Solution - Option 2: Manual Synchronization:
// ✅ GOOD: Manual synchronization for complex scenarios
EventObserver<BeforeRequestSentEventArgs> observer =
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await SaveRequestToFileAsync(e.Request);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
await driver.Session.SubscribeAsync(subscribeParams);
observer.StartCapturingTasks();
await driver.BrowsingContext.NavigateAsync(navParams);
// Wait for 5 events to arrive
Task[] handlerTasks = await observer.WaitForCapturedTasksAsync(5, TimeSpan.FromSeconds(10));
bool fulfilled = handlerTasks.Length == 5;
if (fulfilled)
{
// Inspect or manipulate tasks if needed
Console.WriteLine($"Waiting for {handlerTasks.Length} handlers to complete...");
// Wait for all handlers to finish
await Task.WhenAll(handlerTasks);
}
observer.StopCapturingTasks();
The Solution - Option 3: GetCapturedTasks for Custom Control:
// ✅ GOOD: GetCapturedTasks for fine-grained control
EventObserver<BeforeRequestSentEventArgs> observer =
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await SaveRequestToFileAsync(e.Request);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
await driver.Session.SubscribeAsync(subscribeParams);
observer.StartCapturingTasks();
await driver.BrowsingContext.NavigateAsync(navParams);
// Drain whatever tasks have arrived so far
Task[] tasks = observer.GetCapturedTasks();
await Task.WhenAll(tasks);
observer.StopCapturingTasks();
Note:
GetCapturedTasks()blocks the calling thread to acquire an internal reader lock. If a concurrentWaitForCapturedTasksAsync()call holds the lock,GetCapturedTasks()will block the calling thread until that wait completes, times out, or the observer is disposed. In environments with a single-threadedSynchronizationContext(such as WPF or legacy ASP.NET), that block ties up the only thread available to the context, so preferWaitForCapturedTasksAsync()there.
Key Takeaway: With async handlers, use WaitForCapturedTasksCompleteAsync() or WaitForCapturedTasksAsync() with manual task management to ensure handlers complete before your code continues.
Transport Error Behavior
Pitfall: Not Understanding Default Error Handling
The Problem:
By default, transport-level errors (invalid protocol messages, event handler exceptions) are ignored. This can hide bugs in your event handlers.
// ❌ PROBLEM: By default a handler exception is never thrown to your code
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// If this throws, nothing is thrown or collected by default
ProcessLogEntry(e); // Might throw
});
await driver.Session.SubscribeAsync(subscribeParams);
await driver.BrowsingContext.NavigateAsync(navParams);
// Unless something observes driver.OnEventHandlerErrorOccurred, you'll never know it threw!
Why Default is Ignore:
The library defaults to TransportErrorBehavior.Ignore to prevent event handler exceptions from disrupting automation. However, this can mask bugs during development.
The Solution - For Development: Use Terminate or Collect:
// ✅ Option 1: Terminate mode (throws on next command)
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection);
// Change error behavior
transport.EventHandlerExceptionBehavior = TransportErrorBehavior.Terminate;
transport.ProtocolErrorBehavior = TransportErrorBehavior.Terminate;
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Add handler that might throw
driver.Log.OnEntryAdded.AddObserver((e) => ProcessLogEntry(e));
await driver.Session.SubscribeAsync(subscribeParams);
// If a handler threw, the next command is where it surfaces
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (AggregateException ex)
{
// More than one error accumulated before the next command: each is an inner exception.
Console.WriteLine($"Event handler errors: {string.Join(", ", ex.InnerExceptions.Select(inner => inner.Message))}");
}
catch (WebDriverBiDiException ex)
{
// Exactly one error accumulated.
Console.WriteLine($"Event handler error: {ex.Message}");
}
This also applies to exceptions from handlers using ObservableEventHandlerOptions.RunHandlerAsynchronously when those tasks are not captured by a capture session. If you instead capture handler tasks using WaitForCapturedTasksAsync() or WaitForCapturedTasksCompleteAsync(), those exceptions are owned by the returned tasks and should be observed there.
// ✅ Option 2: Collect mode (gather all errors)
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection);
transport.EventHandlerExceptionBehavior = TransportErrorBehavior.Collect;
transport.ProtocolErrorBehavior = TransportErrorBehavior.Collect;
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
driver.Log.OnEntryAdded.AddObserver((e) => ProcessLogEntry(e));
await driver.Session.SubscribeAsync(subscribeParams);
await driver.BrowsingContext.NavigateAsync(navParams);
try
{
await driver.StopAsync();
}
catch (AggregateException ex)
{
// Check collected errors
if (ex.InnerExceptions.Count > 0)
{
Console.WriteLine($"Collected {ex.InnerExceptions.Count} errors:");
foreach (var error in ex.InnerExceptions)
{
Console.WriteLine($" - {error.Message}");
}
}
}
finally
{
await driver.DisposeAsync();
}
Note: With
Collect, the errors are thrown only byStopAsync().DisposeAsync()catches them, logs them atWarnlevel, and discards them—soawait using var driver = ...on its own never surfaces collected errors. CallStopAsync()explicitly (as above) before disposal. The BIDI012 analyzer warns when it sees aCollectbehavior set in a method that disposes the driver without stopping it.
The Solution - For Production: Handle Exceptions in Handlers:
// ✅ BEST: Handle exceptions inside handlers
driver.Log.OnEntryAdded.AddObserver((e) =>
{
try
{
ProcessLogEntry(e);
}
catch (Exception ex)
{
// Log error, but don't let it propagate
Console.WriteLine($"Error processing log entry: {ex.Message}");
}
});
Error Behavior Modes:
| Mode | Behavior | Best For |
|---|---|---|
| Ignore (default) | Errors neither collected nor thrown; still reported through the diagnostic events (see Ignore Mode) | Production (with try-catch in handlers) |
| Collect | Errors stored in list; thrown by StopAsync() only (discarded by DisposeAsync()) |
Development, diagnostics |
| Terminate | Throws on next command | Development, fast failure |
Key Takeaway: Default error behavior is Ignore. During development, use Terminate or Collect mode to catch handler bugs. In production, handle exceptions within your handlers.
Thread Safety
Pitfall: Assuming All Operations Are Thread-Safe
The Problem:
While many operations in WebDriverBiDi.NET are thread-safe, not all concurrent scenarios are supported.
What IS Thread-Safe:
BiDiDriver.RegisterModule()- Command execution (
ExecuteCommandAsync) - Event observer notification
- Adding and removing observers (
AddObserver,RemoveObserver,Unobserve) on the same event - Transport message processing
- EventObserver capture API (
StartCapturingTasks,StopCapturingTasks,WaitForCapturedTasksAsync,WaitForCapturedTasksCompleteAsync,GetCapturedTasks) - individually thread-safe; only one capture session per observer at a time; the channel queues captured tasks so that no events are missed even if they arrive before the consumer is ready
What to Be Careful With:
- Modifying shared state from multiple event handlers
- Concurrent access to command parameter objects
The Solution:
// ✅ GOOD: Register modules before concurrent operations
driver.RegisterModule(module1);
driver.RegisterModule(module2);
await driver.StartAsync(url);
// ✅ GOOD: Add observers sequentially during setup
driver.Log.OnEntryAdded.AddObserver(handler1);
driver.Log.OnEntryAdded.AddObserver(handler2);
// ✅ GOOD: Execute commands concurrently (this IS safe)
Task<NavigateCommandResult> nav1 =
driver.BrowsingContext.NavigateAsync(params1);
Task<NavigateCommandResult> nav2 =
driver.BrowsingContext.NavigateAsync(params2);
await Task.WhenAll(nav1, nav2);
// ✅ GOOD: Use locks for shared state in handlers
object stateLock = new();
int counter = 0;
driver.Log.OnEntryAdded.AddObserver((e) =>
{
lock (stateLock)
{
counter++;
}
});
Key Takeaway: Transport processing, command execution, and observer registration/removal are thread-safe. You can parallelize those operations, but still protect any shared mutable application state used by handlers.
Resource Cleanup
Pitfall: Not Disposing Observers and Driver
The Problem:
Event observers and the driver hold resources that should be properly disposed.
// ❌ BAD: No cleanup
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(url);
EventObserver<EntryAddedEventArgs> observer =
driver.Log.OnEntryAdded.AddObserver(handler);
// ... use driver ...
// Oops! Never stopped driver or removed observer
// Resources leaked!
The Solution:
Always clean up resources:
// ✅ GOOD: Proper cleanup
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
try
{
await driver.StartAsync(url);
EventObserver<EntryAddedEventArgs> observer =
driver.Log.OnEntryAdded.AddObserver(handler);
try
{
await driver.Session.SubscribeAsync(subscribeParams);
// ... use driver ...
}
finally
{
// Remove observer when done
observer.Unobserve();
}
}
finally
{
// Always stop driver
if (driver.IsStarted)
{
await driver.StopAsync();
}
}
// ✅ BETTER: Use async disposal
await using BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(url);
using EventObserver<EntryAddedEventArgs> observer =
driver.Log.OnEntryAdded.AddObserver(handler);
// Automatic cleanup when scope exits
Key Takeaway: Use try-finally blocks or using/await using statements to ensure proper cleanup of observers and the driver.
Summary Checklist
Before running your WebDriverBiDi.NET code, verify:
Tip: Add the WebDriverBiDi.Analyzers package to get compile-time diagnostics for many of these pitfalls.
- [ ] Event handlers use
RunHandlerAsynchronouslyfor I/O operations - [ ] You've called both
AddObserver()ANDSession.SubscribeAsync() - [ ] Modules, custom events, and type resolvers registered BEFORE
StartAsync(); observers added BEFORESession.SubscribeAsync() - [ ] Nullable collections (
Headers/Cookieson the network continue/provide commands, and theClientHintsMetadatabrand and form-factor lists) checked for null before adding items; every other list populated with.Add()or a collection initializer - [ ] Timeout configured appropriately for your use case
- [ ] Async handlers synchronized with capture API if needed
- [ ] Transport error behavior set for development/production
- [ ] Using correct WebSocket URL format (
ws://) - [ ] Thread safety considered for concurrent operations
- [ ] Resources properly cleaned up with try-finally or using statements
See Also
- Roslyn Analyzers: Compile-time diagnostics for common pitfalls
- Error Handling: Troubleshooting, timeout patterns, TransportErrorBehavior
- API Design Guide: Timeout and cancellation patterns
Next Steps
- Events and Observables: Deep dive into event handling
- Error Handling: Comprehensive error management strategies
- Core Concepts: Understanding the fundamentals
- Architecture: System design and patterns