Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/four-hoops-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

add custom error classes
3 changes: 2 additions & 1 deletion evals/taskConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import fs from "fs";
import path from "path";
import { AvailableModel, AvailableModelSchema } from "@/dist";
import { filterByEvalName } from "./args";
import { UnsupportedModelError } from "@/types/stagehandErrors";

// The configuration file `evals.config.json` contains a list of tasks and their associated categories.
const configPath = path.join(__dirname, "evals.config.json");
Expand Down Expand Up @@ -62,7 +63,7 @@ const getModelList = (): string[] => {
};
const MODELS: AvailableModel[] = getModelList().map((model) => {
if (!AvailableModelSchema.safeParse(model).success) {
throw new Error(`Model ${model} is not a supported model`);
throw new UnsupportedModelError(getModelList(), "Running evals");
}
return model as AvailableModel;
});
Expand Down
49 changes: 29 additions & 20 deletions lib/StagehandPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ import { LLMClient } from "./llm/LLMClient";
import { StagehandContext } from "./StagehandContext";
import { EnhancedContext } from "../types/context";
import { clearOverlays } from "./utils";
import {
StagehandError,
StagehandNotInitializedError,
StagehandEnvironmentError,
CaptchaTimeoutError,
StagehandNotImplementedError,
StagehandDeprecationError,
BrowserbaseSessionNotFoundError,
MissingLLMConfigurationError,
HandlerNotInitializedError,
} from "../types/stagehandErrors";

const BROWSERBASE_REGION_DOMAIN = {
"us-west-2": "wss://connect.usw2.browserbase.com",
Expand Down Expand Up @@ -61,9 +72,7 @@ export class StagehandPage {
prop === ("on" as keyof Page))
) {
return () => {
throw new Error(
`You seem to be calling \`${String(prop)}\` on a page in an uninitialized \`Stagehand\` object. Ensure you are running \`await stagehand.init()\` on the Stagehand object before referencing the \`page\` object.`,
);
throw new StagehandNotInitializedError(String(prop));
};
}

Expand Down Expand Up @@ -121,7 +130,7 @@ export class StagehandPage {

const sessionId = this.stagehand.browserbaseSessionID;
if (!sessionId) {
throw new Error("No Browserbase session ID found");
throw new BrowserbaseSessionNotFoundError();
}

const browserbase = new Browserbase({
Expand Down Expand Up @@ -165,14 +174,16 @@ export class StagehandPage {
* Waits for a captcha to be solved when using Browserbase environment.
*
* @param timeoutMs - Optional timeout in milliseconds. If provided, the promise will reject if the captcha solving hasn't started within the given time.
* @throws Error if called in a LOCAL environment
* @throws Error if the timeout is reached before captcha solving starts
* @throws StagehandEnvironmentError if called in a LOCAL environment
* @throws CaptchaTimeoutError if the timeout is reached before captcha solving starts
* @returns Promise that resolves when the captcha is solved
*/
public async waitForCaptchaSolve(timeoutMs?: number) {
if (this.stagehand.env === "LOCAL") {
throw new Error(
"The waitForCaptcha method may only be used when using the Browserbase environment.",
throw new StagehandEnvironmentError(
this.stagehand.env,
"BROWSERBASE",
"waitForCaptcha method",
);
}

Expand All @@ -189,7 +200,7 @@ export class StagehandPage {
if (timeoutMs) {
timeoutId = setTimeout(() => {
if (!started) {
reject(new Error("Captcha timeout"));
reject(new CaptchaTimeoutError());
}
}, timeoutMs);
}
Expand Down Expand Up @@ -228,9 +239,7 @@ export class StagehandPage {
if (prop === "act" || prop === "extract" || prop === "observe") {
if (!this.llmClient) {
return () => {
throw new Error(
"No LLM API key or LLM Client configured. An LLM API key or a custom LLM Client is required to use act, extract, or observe.",
);
throw new MissingLLMConfigurationError();
};
}

Expand Down Expand Up @@ -444,7 +453,7 @@ export class StagehandPage {
actionOrOptions: string | ActOptions | ObserveResult,
): Promise<ActResult> {
if (!this.actHandler) {
throw new Error("Act handler not initialized");
throw new HandlerNotInitializedError("Act");
}

await clearOverlays(this.page);
Expand All @@ -461,7 +470,7 @@ export class StagehandPage {
// If it's an object but no selector/method,
// check that it's truly ActOptions (i.e., has an `action` field).
if (!("action" in actionOrOptions)) {
throw new Error(
throw new StagehandError(
"Invalid argument. Valid arguments are: a string, an ActOptions object, " +
"or an ObserveResult WITH 'selector' and 'method' fields.",
);
Expand All @@ -471,7 +480,7 @@ export class StagehandPage {
// Convert string to ActOptions
actionOrOptions = { action: actionOrOptions };
} else {
throw new Error(
throw new StagehandError(
"Invalid argument: you may have called act with an empty ObserveResult.\n" +
"Valid arguments are: a string, an ActOptions object, or an ObserveResult " +
"WITH 'selector' and 'method' fields.",
Expand Down Expand Up @@ -580,7 +589,7 @@ export class StagehandPage {
instructionOrOptions?: string | ExtractOptions<T>,
): Promise<ExtractResult<T>> {
if (!this.extractHandler) {
throw new Error("Extract handler not initialized");
throw new HandlerNotInitializedError("Extract");
}

await clearOverlays(this.page);
Expand Down Expand Up @@ -614,8 +623,8 @@ export class StagehandPage {
// Throw a NotImplementedError if the user passed in an `xpath`
// and `useTextExtract` is false
if (selector && useTextExtract !== true) {
throw new Error(
"NotImplementedError: Passing an xpath into extract is only supported when `useTextExtract: true`.",
throw new StagehandNotImplementedError(
"Passing an xpath into extract is only supported when `useTextExtract: true`.",
);
}

Expand Down Expand Up @@ -687,7 +696,7 @@ export class StagehandPage {
instructionOrOptions?: string | ObserveOptions,
): Promise<ObserveResult[]> {
if (!this.observeHandler) {
throw new Error("Observe handler not initialized");
throw new HandlerNotInitializedError("Observe");
}

await clearOverlays(this.page);
Expand Down Expand Up @@ -719,7 +728,7 @@ export class StagehandPage {
" 2. Don't declare useAccessibilityTree",
level: 1,
});
throw new Error(
throw new StagehandDeprecationError(
"useAccessibilityTree is deprecated. Use onlyVisible instead.",
);
}
Expand Down
24 changes: 17 additions & 7 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import {
ObserveResult,
} from "../types/stagehand";
import { AgentExecuteOptions, AgentResult } from ".";
import {
StagehandAPIUnauthorizedError,
StagehandHttpError,
StagehandAPIError,
StagehandServerError,
StagehandResponseBodyError,
StagehandResponseParseError,
} from "../types/stagehandApiErrors";

export class StagehandAPI {
private apiKey: string;
Expand Down Expand Up @@ -63,19 +71,19 @@ export class StagehandAPI {
});

if (sessionResponse.status === 401) {
throw new Error(
throw new StagehandAPIUnauthorizedError(
"Unauthorized. Ensure you provided a valid API key and that it is whitelisted.",
);
} else if (sessionResponse.status !== 200) {
console.log(await sessionResponse.text());
throw new Error(`Unknown error: ${sessionResponse.status}`);
throw new StagehandHttpError(`Unknown error: ${sessionResponse.status}`);
}

const sessionResponseBody =
(await sessionResponse.json()) as ApiResponse<StartSessionResult>;

if (sessionResponseBody.success === false) {
throw new Error(sessionResponseBody.message);
throw new StagehandAPIError(sessionResponseBody.message);
}

this.sessionId = sessionResponseBody.data.sessionId;
Expand Down Expand Up @@ -153,13 +161,13 @@ export class StagehandAPI {

if (!response.ok) {
const errorBody = await response.text();
throw new Error(
throw new StagehandHttpError(
`HTTP error! status: ${response.status}, body: ${errorBody}`,
);
}

if (!response.body) {
throw new Error("Response body is null");
throw new StagehandResponseBodyError();
}

const reader = response.body.getReader();
Expand All @@ -185,7 +193,7 @@ export class StagehandAPI {

if (eventData.type === "system") {
if (eventData.data.status === "error") {
throw new Error(eventData.data.error);
throw new StagehandServerError(eventData.data.error);
}
if (eventData.data.status === "finished") {
return eventData.data.result as T;
Expand All @@ -195,7 +203,9 @@ export class StagehandAPI {
}
} catch (e) {
console.error("Error parsing event data:", e);
throw new Error("Failed to parse server response");
throw new StagehandResponseParseError(
"Failed to parse server response",
);
}
}

Expand Down
14 changes: 10 additions & 4 deletions lib/handlers/actHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
} from "./handlerUtils/actHandlerUtils";
import { Stagehand } from "@/lib";
import { StagehandObserveHandler } from "@/lib/handlers/observeHandler";
import {
StagehandElementNotFoundError,
StagehandInvalidArgumentError,
} from "@/types/stagehandErrors";
/**
* NOTE: Vision support has been removed from this version of Stagehand.
* If useVision or verifierUseVision is set to true, a warning is logged and
Expand Down Expand Up @@ -224,7 +228,7 @@ export class StagehandActHandler {

if (typeof actionOrOptions === "object" && actionOrOptions !== null) {
if (!("action" in actionOrOptions)) {
throw new Error(
throw new StagehandInvalidArgumentError(
"Invalid argument. Action options must have an `action` field.",
);
}
Expand All @@ -233,7 +237,9 @@ export class StagehandActHandler {
typeof actionOrOptions.action !== "string" ||
actionOrOptions.action.length === 0
) {
throw new Error("Invalid argument. No action provided.");
throw new StagehandInvalidArgumentError(
"Invalid argument. No action provided.",
);
}

action = actionOrOptions.action;
Expand All @@ -244,7 +250,7 @@ export class StagehandActHandler {
if (actionOrOptions.modelClientOptions)
observeOptions.modelClientOptions = actionOrOptions.modelClientOptions;
} else {
throw new Error(
throw new StagehandInvalidArgumentError(
"Invalid argument. Valid arguments are: a string, an ActOptions object with an `action` field not empty, or an ObserveResult with a `selector` and `method` field.",
);
}
Expand Down Expand Up @@ -745,7 +751,7 @@ export class StagehandActHandler {

// If no XPath was valid, we cannot proceed
if (!foundXpath || !locator) {
throw new Error("None of the provided XPaths could be located.");
throw new StagehandElementNotFoundError(xpaths);
}

const originalUrl = this.stagehandPage.page.url();
Expand Down
Loading