# How the `cdp()` Function Evaluates JavaScript Expressions in ego-lite: A Deep Dive into Chrome DevTools Protocol Integration

> Discover how ego-lite's cdp() function evaluates JavaScript expressions using Chrome DevTools Protocol. Understand Runtime.evaluate with automatic serialization and error handling.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-08-01

---

**The `cdp()` function in ego-lite is a low-level bridge that sends raw Chrome DevTools Protocol (CDP) commands to the embedded browser, enabling JavaScript evaluation through the `Runtime.evaluate` method with automatic serialization and error handling.**

The `cdp()` function serves as the foundation for all browser automation in [ego-lite](https://github.com/citrolabs/ego-lite), a lightweight browser control library. Understanding how JavaScript expression evaluation works in ego-lite requires tracing the path from high-level APIs down to the raw CDP transport layer.

## The `cdp()` Function: Raw CDP Command Sender

At the core of expression evaluation lies the `cdp()` helper in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts). This function dispatches arbitrary CDP commands and returns their results.

```ts
export async function cdp(method, params: any = {}, sessionId = undefined) {
  const result = state.cdpOverride
    ? await state.cdpOverride(method, params, sessionId)
    : (await send({ method, params, session_id: sessionId })).result || {};
  // Mirror Network domain state when the default session is used
  if (!sessionId && (method === "Network.enable" || method === "Network.disable")) {
    state.networkDomainEnabled = method === "Network.enable";
  }
  return result;
}

```

*Source:* [`src/cdp-eval.ts#L12-L26`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts#L12-L26)

Key behaviors of `cdp()`:

- **Test override support**: If `state.cdpOverride` is set, it intercepts all calls for mocking or debugging
- **Direct transport**: Otherwise, commands route through `send()` (defined in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts))
- **Network state tracking**: Automatically mirrors `Network.enable`/`Network.disable` calls in `state.networkDomainEnabled`

## The `evaluate()` API: User-Facing JavaScript Execution

Most agent scripts call `evaluate()` rather than `cdp()` directly. This higher-level function handles function serialization, argument passing, and automatic IIFE wrapping.

```ts
export async function evaluate(pageFunction, arg = undefined) {
  // …constructs a JavaScript expression string…
  if (hasReturnStatement(expression) && !expression.trim().startsWith("(")) {
    expression = `(function(){${expression}})()`;
  }
  return runtimeEvaluate(expression, sessionId, true);
}

```

*Source:* [`src/cdp-eval.ts#L29-L65`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts#L29-L65)

`evaluate()` accepts two forms of input:

- **JavaScript functions**: Stringified and invoked with serialized arguments
- **String expressions**: Raw code executed in page context

For string expressions with `return` statements at the top level, ego-lite automatically wraps them in an immediately-invoked function expression (IIFE). This matches Playwright's behavior and prevents syntax errors when returning values.

## The `runtimeEvaluate()` CDP Call

The actual browser execution happens in `runtimeEvaluate()`, which constructs and sends the final `Runtime.evaluate` command.

```ts
const response = await cdp(
  "Runtime.evaluate",
  { expression, returnByValue: true, awaitPromise },
  sessionId,
);
return runtimeValue(response, expression);

```

*Source:* [`src/cdp-eval.ts#L67-L82`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts#L67-L82)

Critical parameters passed to `Runtime.evaluate`:

- **`returnByValue: true`**: Forces Chrome to serialize the result rather than returning a remote object reference
- **`awaitPromise`**: Boolean controlling whether the runtime should await a returned Promise before resolving

## The `runtimeValue()` Result Handler

After the browser responds, `runtimeValue()` transforms the CDP payload into a native JavaScript value.

```ts
const result = response.result || {};
const details = response.exceptionDetails;
if (details || result.subtype === "error") {
  // throws a descriptive error
}
if (Object.hasOwn(result, "value")) return result.value;
if (Object.hasOwn(result, "unserializableValue"))
  return decodeUnserializableJsValue(result.unserializableValue);
return null;

```

*Source:* [`src/cdp-eval.ts#L96-L115`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts#L96-L115)

The result handling path covers three cases:

1. **Exceptions**: Surfaced as descriptive errors with stack traces
2. **Serializable values**: Returned directly via `result.value`
3. **Unserializable values**: Decoded through `decodeUnserializableJsValue()` for types like `NaN`, `Infinity`, and `BigInt`

## Practical Code Examples

Using `evaluate()` with string expressions:

```ts
// Simple property access
const title = await evaluate('document.title');
// Returns: "My Page"

```

Passing functions with arguments:

```ts
// Function with serialized arguments
const sum = await evaluate((a, b) => a + b, [3, 7]);
// Returns: 10

```

Awaiting page-side Promises:

```ts
// Async evaluation with automatic promise handling
const result = await evaluate(async () => {
  const data = await fetch('/api/info').then(r => r.json());
  return data.total;
});
// Returns: resolved value from the page

```

## How Session IDs Enable Multi-Target Evaluation

When `evaluate()` detects a target ID passed as the optional second argument, it automatically attaches a temporary CDP session via `Target.attachToTarget`. This enables script execution in specific iframes, workers, or spawned tabs without requiring manual session management.

The `sessionId` flows through the entire chain: `evaluate()` → `runtimeEvaluate()` → `cdp()` → `send()`, ensuring commands route to the correct browser execution context.

## Key Source Files

| File | Responsibility |
|------|--------------|
| [[`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts) | Core implementation: `cdp`, `evaluate`, `runtimeEvaluate`, `runtimeValue` |
| [[`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/state.ts) | Holds `cdpOverride` and `networkDomainEnabled` flags |
| [[`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | Exports `evaluate` and `js` alias for agent consumption |
| [[`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) | WebSocket/CDP transport layer implementing `send()` |

## Summary

- **`cdp()`** is the transport-layer primitive that sends raw CDP commands through `send()` or a test override
- **`evaluate()`** provides ergonomic JavaScript execution with automatic function serialization and IIFE wrapping
- **`runtimeEvaluate()`** bridges to Chrome's `Runtime.evaluate` with `returnByValue: true` for serialized results
- **`runtimeValue()`** converts CDP response payloads into native JavaScript values, handling exceptions and unserializable types

## Frequently Asked Questions

### What happens if JavaScript execution throws an error in the browser?

ego-lite checks `response.exceptionDetails` and `result.subtype === "error"` in `runtimeValue()`, throwing a descriptive error containing the original stack trace from the browser. This ensures page-side failures surface immediately in your Node.js code.

### Can I evaluate code in a specific iframe or worker?

Yes. Pass a target ID as the second argument to `evaluate()`. ego-lite automatically calls `Target.attachToTarget` to create a session, executes your code in that context, and cleans up afterward.

### What's the difference between `evaluate()` and calling `cdp("Runtime.evaluate", ...)` directly?

`evaluate()` handles JavaScript function serialization, argument passing, automatic IIFE wrapping, and result decoding. Direct `cdp()` calls require you to construct raw CDP parameters and manually process the `Runtime.RemoteObject` response.

### How does ego-lite handle unserializable values like `NaN` or `BigInt`?

When Chrome cannot serialize a value through standard JSON, it returns an `unserializableValue` string. ego-lite's `decodeUnserializableJsValue()` function parses these special strings back into proper JavaScript values.