Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ready-carrots-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": minor
---

Added custom error types to allow for a smoother debugging experience.
7 changes: 5 additions & 2 deletions packages/core/lib/v3/agent/GoogleCUAClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
AgentExecutionOptions,
} from "../types/public/agent";
import { AgentClient } from "./AgentClient";
import { AgentScreenshotProviderError } from "../types/public/sdkErrors";
import {
AgentScreenshotProviderError,
LLMResponseError,
} from "../types/public/sdkErrors";
import { buildGoogleCUASystemPrompt } from "../../prompt";
import { compressGoogleConversationImages } from "./utils/imageCompression";
import { mapKeyToPlaywright } from "./utils/cuaKeyMapping";
Expand Down Expand Up @@ -312,7 +315,7 @@ export class GoogleCUAClient extends AgentClient {

// Check if we have valid response content
if (!response.candidates || response.candidates.length === 0) {
throw new Error("Response has no candidates!");
throw new LLMResponseError("agent", "Response has no candidates!");
}

// Success - we have a valid response
Expand Down
5 changes: 2 additions & 3 deletions packages/core/lib/v3/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
StagehandResponseBodyError,
StagehandResponseParseError,
StagehandServerError,
ExperimentalNotConfiguredError,
} from "./types/public";
import type { SerializableResponse } from "./types/private";

Expand Down Expand Up @@ -224,9 +225,7 @@ export class StagehandAPIClient {
): Promise<AgentResult> {
// Check if integrations are being used in API mode
if (agentConfig.integrations && agentConfig.integrations.length > 0) {
throw new StagehandAPIError(
"MCP integrations are not supported in API mode. Set experimental: true to use MCP integrations.",
);
throw new ExperimentalNotConfiguredError("MCP integrations");
}
if (typeof executeOptions === "object") {
if (executeOptions.page) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/lib/v3/cache/ActCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ActCacheDeps,
CachedActEntry,
} from "../types/private";
import { StagehandNotInitializedError } from "../types/public/sdkErrors";

export class ActCache {
private readonly storage: CacheStorage;
Expand Down Expand Up @@ -164,7 +165,7 @@ export class ActCache {
): Promise<ActResult> {
const handler = this.getActHandler();
if (!handler) {
throw new Error("V3 not initialized. Call init() before act().");
throw new StagehandNotInitializedError("act()");
}

const execute = async (): Promise<ActResult> => {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/lib/v3/handlers/extractHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ClientOptions,
ModelConfiguration,
} from "../types/public/model";
import { StagehandInvalidArgumentError } from "../types/public/sdkErrors";

/**
* Scans the provided Zod schema for any `z.string().url()` fields and
Expand Down Expand Up @@ -105,7 +106,7 @@ export class ExtractHandler {
}

if (!instruction && schema) {
throw new Error(
throw new StagehandInvalidArgumentError(
"extract() requires an instruction when a schema is provided.",
);
}
Expand Down
5 changes: 2 additions & 3 deletions packages/core/lib/v3/handlers/v3AgentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../types/public/agent";
import { V3FunctionName } from "../types/public/methods";
import { mapToolResultToActions } from "../agent/utils/actionMapping";
import { MissingLLMConfigurationError } from "../types/public/sdkErrors";

export class V3AgentHandler {
private v3: V3;
Expand Down Expand Up @@ -65,9 +66,7 @@ export class V3AgentHandler {
];

if (!this.llmClient?.getLanguageModel) {
throw new Error(
"V3AgentHandler requires an AISDK-backed LLM client. Ensure your model is configured like 'openai/gpt-4.1-mini'.",
);
throw new MissingLLMConfigurationError();
}
const baseModel = this.llmClient.getLanguageModel();
const wrappedModel = wrapLanguageModel({
Expand Down
10 changes: 7 additions & 3 deletions packages/core/lib/v3/launch/browserbase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Browserbase from "@browserbasehq/sdk";
import {
BrowserbaseSessionNotFoundError,
StagehandInitError,
} from "../types/public/sdkErrors";

export async function createBrowserbaseSession(
apiKey: string,
Expand All @@ -16,12 +20,12 @@ export async function createBrowserbaseSession(
resumeSessionId,
)) as unknown as { id: string; connectUrl?: string; status?: string };
if (!existing?.id) {
throw new Error(`Browserbase session not found: ${resumeSessionId}`);
throw new BrowserbaseSessionNotFoundError();
}

const ws = existing.connectUrl;
if (!ws) {
throw new Error(
throw new StagehandInitError(
`Browserbase session resume missing connectUrl for ${resumeSessionId}`,
);
}
Expand Down Expand Up @@ -55,7 +59,7 @@ export async function createBrowserbaseSession(
};

if (!created?.connectUrl || !created?.id) {
throw new Error(
throw new StagehandInitError(
"Browserbase session creation returned an unexpected shape.",
);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/lib/v3/launch/local.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { launch, LaunchedChrome } from "chrome-launcher";
import { ConnectionTimeoutError } from "../types/public/sdkErrors";

interface LaunchLocalOptions {
chromePath?: string;
Expand Down Expand Up @@ -60,7 +61,7 @@ async function waitForWebSocketDebuggerUrl(
await new Promise((r) => setTimeout(r, 250));
}

throw new Error(
throw new ConnectionTimeoutError(
`Timed out waiting for /json/version on port ${port}${
lastErrMsg ? ` (last error: ${lastErrMsg})` : ""
}`,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/lib/v3/tests/locator-count-iframe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ test.describe("Locator count() method with iframes", () => {
expect(true).toBe(false);
} catch (error) {
// Expected behavior - frameLocator should throw when iframe doesn't exist
expect(error.message).toContain("Element not found for selector");
expect(error.message).toContain(
"Could not find an element for the given xPath(s):",
);
}
});
});
45 changes: 45 additions & 0 deletions packages/core/lib/v3/types/public/sdkErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ export class ExperimentalNotConfiguredError extends StagehandError {
}
}

export class CuaModelRequiredError extends StagehandError {
constructor(availableModels: readonly string[]) {
super(
`To use the computer use agent (CUA), please provide a CUA model in the agent constructor or stagehand config. ` +
`Try one of our supported CUA models: ${availableModels.join(", ")}`,
);
}
}

export class ZodSchemaValidationError extends Error {
constructor(
public readonly received: unknown,
Expand Down Expand Up @@ -274,3 +283,39 @@ export class StagehandShadowSegmentNotFoundError extends StagehandError {
);
}
}

export class ElementNotVisibleError extends StagehandError {
constructor(selector: string) {
super(`Element not visible (no box model): ${selector}`);
}
}

export class ResponseBodyError extends StagehandError {
constructor(message: string) {
super(`Failed to retrieve response body: ${message}`);
}
}

export class ResponseParseError extends StagehandError {
constructor(message: string) {
super(`Failed to parse response: ${message}`);
}
}

export class TimeoutError extends StagehandError {
constructor(operation: string, timeoutMs: number) {
super(`${operation} timed out after ${timeoutMs}ms`);
}
}

export class PageNotFoundError extends StagehandError {
constructor(identifier: string) {
super(`No Page found for ${identifier}`);
}
}

export class ConnectionTimeoutError extends StagehandError {
constructor(message: string) {
super(`Connection timeout: ${message}`);
}
}
23 changes: 19 additions & 4 deletions packages/core/lib/v3/understudy/a11y/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { CDPSessionLike } from "../cdp";
import { Page } from "../page";
import { executionContexts } from "../executionContextRegistry";
import { v3Logger } from "../../logger";
import { StagehandIframeError } from "../../types/public/sdkErrors";

/**
* a11y/snapshot
Expand Down Expand Up @@ -1005,7 +1006,11 @@ async function resolveFocusFrameAndTail(
selectorForIframe,
ctxFrameId,
);
if (!objectId) throw new Error("Failed to resolve iframe element by XPath");
if (!objectId)
throw new StagehandIframeError(
selectorForIframe,
"Failed to resolve iframe element by XPath",
);

try {
await parentSess.send("DOM.enable").catch(() => {});
Expand All @@ -1031,7 +1036,10 @@ async function resolveFocusFrameAndTail(
}
}
if (!childFrameId)
throw new Error("Could not map iframe to child frameId");
throw new StagehandIframeError(
selectorForIframe,
"Could not map iframe to child frameId",
);

// Update absolute prefix with the iframe element path within the parent document
absPrefix = prefixXPath(absPrefix || "/", selectorForIframe);
Expand Down Expand Up @@ -1083,7 +1091,11 @@ async function resolveCssFocusFrameAndTail(
parts[i]!,
ctxFrameId,
);
if (!objectId) throw new Error("Failed to resolve iframe via CSS hop");
if (!objectId)
throw new StagehandIframeError(
parts[i]!,
"Failed to resolve iframe via CSS hop",
);
try {
await parentSess.send("DOM.enable").catch(() => {});
const desc = await parentSess.send<Protocol.DOM.DescribeNodeResponse>(
Expand All @@ -1107,7 +1119,10 @@ async function resolveCssFocusFrameAndTail(
}
}
if (!childFrameId)
throw new Error("Could not map CSS iframe hop to child frameId");
throw new StagehandIframeError(
parts[i]!,
"Could not map CSS iframe hop to child frameId",
);
ctxFrameId = childFrameId;
} finally {
await parentSess
Expand Down
13 changes: 7 additions & 6 deletions packages/core/lib/v3/understudy/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { installV3PiercerIntoSession } from "./piercer";
import { executionContexts } from "./executionContextRegistry";
import type { StagehandAPIClient } from "../api";
import { LocalBrowserLaunchOptions } from "../types/public";
import { TimeoutError, PageNotFoundError } from "../types/public/sdkErrors";

type TargetId = string;
type SessionId = string;
Expand Down Expand Up @@ -96,8 +97,9 @@ export class V3Context {
}
await new Promise((r) => setTimeout(r, 25));
}
throw new Error(
`waitForFirstTopLevelPage timed out after ${timeoutMs}ms (no top-level Page)`,
throw new TimeoutError(
"waitForFirstTopLevelPage (no top-level Page)",
timeoutMs,
);
}

Expand Down Expand Up @@ -201,8 +203,7 @@ export class V3Context {
rootMainFrameId: string,
): Promise<Protocol.Page.FrameTree> {
const owner = this.resolvePageByMainFrameId(rootMainFrameId);
if (!owner)
throw new Error(`No Page found for mainFrameId=${rootMainFrameId}`);
if (!owner) throw new PageNotFoundError(`mainFrameId=${rootMainFrameId}`);
return owner.asProtocolFrameTree(rootMainFrameId);
}

Expand All @@ -225,7 +226,7 @@ export class V3Context {
if (page) return page;
await new Promise((r) => setTimeout(r, 25));
}
throw new Error(`newPage timeout: target not attached (${targetId})`);
throw new TimeoutError(`newPage: target not attached (${targetId})`, 5000);
}

/**
Expand Down Expand Up @@ -670,6 +671,6 @@ export class V3Context {
await new Promise((r) => setTimeout(r, 25));
}
if (immediate) return immediate;
throw new Error("awaitActivePage: no page available");
throw new PageNotFoundError("awaitActivePage: no page available");
}
}
5 changes: 4 additions & 1 deletion packages/core/lib/v3/understudy/deepLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Frame } from "./frame";
import type { Page } from "./page";
import { v3Logger } from "../logger";
import { FrameLocator, frameLocatorFromFrame } from "./frameLocator";
import { StagehandInvalidArgumentError } from "../types/public/sdkErrors";

/**
* Recognize iframe steps like "iframe" or "iframe[2]" in an XPath.
Expand Down Expand Up @@ -240,7 +241,9 @@ export class DeepLocatorDelegate {
nth(index: number): DeepLocatorDelegate {
const value = Number(index);
if (!Number.isFinite(value) || value < 0) {
throw new Error("deepLocator().nth() expects a non-negative index");
throw new StagehandInvalidArgumentError(
"deepLocator().nth() expects a non-negative index",
);
}

const nextIndex = Math.floor(value);
Expand Down
5 changes: 4 additions & 1 deletion packages/core/lib/v3/understudy/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Protocol } from "devtools-protocol";
import type { CDPSessionLike } from "./cdp";
import { Locator } from "./locator";
import { StagehandEvalError } from "../types/public/sdkErrors";

interface FrameManager {
session: CDPSessionLike;
Expand Down Expand Up @@ -150,7 +151,9 @@ export class Frame implements FrameManager {
},
);
if (res.exceptionDetails) {
throw new Error(res.exceptionDetails.text ?? "Evaluation failed");
throw new StagehandEvalError(
res.exceptionDetails.text ?? "Evaluation failed",
);
}
return res.result.value as R;
}
Expand Down
5 changes: 2 additions & 3 deletions packages/core/lib/v3/understudy/frameLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Locator } from "./locator";
import type { Page } from "./page";
import { Frame } from "./frame";
import { executionContexts } from "./executionContextRegistry";
import { ContentFrameNotFoundError } from "../types/public/sdkErrors";

/**
* FrameLocator: resolves iframe elements to their child Frames and allows
Expand Down Expand Up @@ -72,9 +73,7 @@ export class FrameLocator {
// ignore and try next
}
}
throw new Error(
`frameLocator: could not resolve child frame for selector: ${this.selector}`,
);
throw new ContentFrameNotFoundError(this.selector);
} finally {
await parentSession
.send("Runtime.releaseObject", { objectId })
Expand Down
3 changes: 2 additions & 1 deletion packages/core/lib/v3/understudy/lifecycleWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { LoadState } from "../types/public/page";
import type { CDPSessionLike } from "./cdp";
import type { NetworkManager } from "./networkManager";
import type { Page } from "./page";
import { TimeoutError } from "../types/public/sdkErrors";
import {
DEFAULT_IDLE_WAIT,
IGNORED_RESOURCE_TYPES,
Expand Down Expand Up @@ -201,7 +202,7 @@ export class LifecycleWatcher {
private timeRemaining(deadline: number): number {
const remaining = deadline - Date.now();
if (remaining <= 0) {
throw new Error(`Lifecycle wait timed out after ${this.timeoutMs}ms`);
throw new TimeoutError("Lifecycle wait", this.timeoutMs);
}
return remaining;
}
Expand Down
Loading