Error Types
Tool errors need to carry more than just a message. A failed login is different from a rate limit, which is different from a missing resource — and an AI agent needs to know which is which. I designed ToolError with machine-readable metadata (code, category, retryable, retryAfterMs) so the agent can make intelligent decisions: retry with backoff, ask the user to log in, or give up immediately. When your tool throws a ToolError, all of that metadata propagates through the entire dispatch chain to the agent.
Import
import { ToolError } from '@opentabs-dev/plugin-sdk';
import type { ErrorCategory, ToolErrorOptions } from '@opentabs-dev/plugin-sdk';ToolError
Extends the standard Error class with machine-readable metadata.
class ToolError extends Error {
readonly code: string;
readonly retryable: boolean;
readonly retryAfterMs: number | undefined;
readonly category: ErrorCategory | undefined;
constructor(message: string, code: string, opts?: ToolErrorOptions);
static auth(message: string, code?: string): ToolError;
static notFound(message: string, code?: string): ToolError;
static rateLimited(message: string, retryAfterMs?: number, code?: string): ToolError;
static validation(message: string, code?: string): ToolError;
static timeout(message: string, code?: string): ToolError;
static internal(message: string, code?: string): ToolError;
}Constructor
new ToolError(message: string, code: string, opts?: ToolErrorOptions)| Parameter | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Human-readable error message. |
code | string | Yes | Machine-readable error code (e.g., 'CHANNEL_NOT_FOUND'). |
opts | ToolErrorOptions | No | Structured metadata (see below). |
ToolErrorOptions
interface ToolErrorOptions {
retryable?: boolean; // Default: false
retryAfterMs?: number; // Suggested retry delay in milliseconds
category?: ErrorCategory;
}Properties
| Property | Type | Description |
|---|---|---|
code | string | Machine-readable error code. Factory methods set this automatically. |
retryable | boolean | Whether the AI agent should retry. Defaults to false. |
retryAfterMs | number \| undefined | Suggested delay before retrying, in milliseconds. |
category | ErrorCategory \| undefined | Error classification. One of: 'auth', 'rate_limit', 'not_found', 'validation', 'internal', 'timeout'. |
Factory Methods
Prefer factory methods over the constructor — they set the correct code, category, and retryable values automatically.
ToolError.auth(message, code?)
Authentication or authorization error. Not retryable — the user needs to log in or fix permissions.
static auth(message: string, code?: string): ToolError;
// Sets: code=code??'AUTH_ERROR', category='auth', retryable=falsethrow ToolError.auth('Not logged in — open the app and sign in first');ToolError.notFound(message, code?)
Resource not found. Not retryable — the item doesn't exist. Accepts an optional domain-specific code (defaults to 'NOT_FOUND').
static notFound(message: string, code?: string): ToolError;
// Sets: code=code??'NOT_FOUND', category='not_found', retryable=falsethrow ToolError.notFound(`Channel "${name}" not found`);
throw ToolError.notFound(`User "${id}" not found`, 'USER_NOT_FOUND');ToolError.rateLimited(message, retryAfterMs?, code?)
Rate limited by the target API. Retryable — the agent should wait and try again. If you know the retry delay, pass retryAfterMs so the agent waits the right amount of time.
static rateLimited(message: string, retryAfterMs?: number, code?: string): ToolError;
// Sets: code=code??'RATE_LIMITED', category='rate_limit', retryable=truethrow ToolError.rateLimited('Too many requests');
throw ToolError.rateLimited('Rate limited by Slack API', 2000);ToolError.validation(message, code?)
Input validation error. Not retryable — the agent needs to fix its input.
static validation(message: string, code?: string): ToolError;
// Sets: code=code??'VALIDATION_ERROR', category='validation', retryable=falsethrow ToolError.validation('Channel name cannot be empty');ToolError.timeout(message, code?)
Operation timed out. Retryable — the operation may succeed on the next attempt.
static timeout(message: string, code?: string): ToolError;
// Sets: code=code??'TIMEOUT', category='timeout', retryable=truethrow ToolError.timeout('Dashboard took too long to load');ToolError.internal(message, code?)
Internal or unexpected error. Not retryable — this indicates a bug in the plugin.
static internal(message: string, code?: string): ToolError;
// Sets: code=code??'INTERNAL_ERROR', category='internal', retryable=falsethrow ToolError.internal('Unexpected response format from API');Factory Summary
| Factory | Code | Category | Retryable |
|---|---|---|---|
ToolError.auth() | AUTH_ERROR | auth | No |
ToolError.notFound() | NOT_FOUND | not_found | No |
ToolError.rateLimited() | RATE_LIMITED | rate_limit | Yes |
ToolError.validation() | VALIDATION_ERROR | validation | No |
ToolError.timeout() | TIMEOUT | timeout | Yes |
ToolError.internal() | INTERNAL_ERROR | internal | No |
ErrorCategory
The six standard error categories:
type ErrorCategory = 'auth' | 'rate_limit' | 'not_found' | 'validation' | 'internal' | 'timeout';Propagation
When a tool handler throws a ToolError, the structured metadata propagates through the entire dispatch chain:
- Tool handler throws
ToolErrorin the page context - Adapter IIFE serializes the error fields (
code,category,retryable,retryAfterMs) into the dispatch response - Chrome extension relays the structured error over WebSocket to the MCP server
- MCP server formats the error response with both a human-readable prefix and a machine-readable JSON block
The AI agent receives an error response like:
[ERROR code=RATE_LIMITED category=rate_limit retryable=true retryAfterMs=2000] Too many requests
```json
{"code":"RATE_LIMITED","category":"rate_limit","retryable":true,"retryAfterMs":2000}
```
The first line is human-readable. The JSON block in the fenced code block is machine-parseable, enabling AI agents to programmatically decide whether to retry, how long to wait, and what kind of error occurred.
Custom Error Codes
Use the constructor directly when the factory methods don't fit your use case:
throw new ToolError(
'Export exceeds 10,000 row limit',
'EXPORT_TOO_LARGE',
{ category: 'validation', retryable: false },
);For most cases, prefer the factory methods — they ensure consistent codes and categories across the ecosystem. See the Error Handling guide for best practices and patterns.
Last Updated: 10 Mar, 2026