Errors
Failures from a vendor go through normalizeError, so a 401 from Steel and a 401 from Cloudflare are the same AuthError, and nearly everything else the package throws extends BrowserError too. Catch the class, not the message. Nearly is the honest word: a few guards still throw a plain Error, like a Playwright screenshot without a session or No configured provider supports … from the resolver, so keep a last branch that rethrows.
import { AuthError, RateLimitError, create } from "@agntn/browsers";
try {
await (await create("steel")).scrape("https://example.com");
} catch (error) {
if (error instanceof RateLimitError) {
await new Promise((resolve) => setTimeout(resolve, error.retryAfter * 1000));
}
else if (error instanceof AuthError) console.error(`fix the key for ${error.provider}`);
else throw error;
}
Fix the call
| Class | When |
|---|---|
UnknownProviderError | The name isn't in the registry. Unknown provider: nope |
InvalidInputError | The arguments contradict each other, like selector with fullPage: true |
UnsupportedOperationError | The provider can't do it. Anchor asked to scrape |
Fix the setup
| Class | When |
|---|---|
AuthError | Key missing, or the vendor said 401. Missing API key for steel. Set STEEL_API_KEY |
PaymentError | The vendor said 402 or 403. Usually a plan or a balance |
NoProviderConfiguredError | No key set for anything and nothing registered to fall back on |
Wait, or try elsewhere
| Class | When |
|---|---|
RateLimitError | A 429. retryAfter holds the seconds, and the message names which provider refused and why |
TimeoutError | No answer in time. Says how long it waited and from where |
HTTPError | Any other status. statusCode, url and the vendor's reason |
SessionNotFoundError | The session is gone, released or expired on the vendor's side |
The package also exports EmptyUrlError, ScrapeNotSupportedError, SessionLimitError and NoProviderAvailableError. Nothing built in throws them today, so don't write a catch for one. A provider of your own can still use them.
A rate limit and a timeout are different problems, and they come back as different classes. One says slow down, the other says nobody answered. Mixing them up is how an agent ends up hammering a vendor that already said please stop.
Keys never leak into a message
Errors carry the request URL, so you can see which vendor failed. Query params like token and api_key, and a username or password in the URL, are replaced with [REDACTED] first:
HTTP 400 from https://chrome.browserless.io/screenshot?token=%5BREDACTED%5D: Element not found on page! (requestId: …)
That line is real, from a Browserless element screenshot of a selector the page didn't have. Only the request ID is cut.