Skip to main content

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)
ParameterTypeRequiredDescription
messagestringYesHuman-readable error message.
codestringYesMachine-readable error code (e.g., 'CHANNEL_NOT_FOUND').
optsToolErrorOptionsNoStructured metadata (see below).

ToolErrorOptions

interface ToolErrorOptions {
  retryable?: boolean;     // Default: false
  retryAfterMs?: number;   // Suggested retry delay in milliseconds
  category?: ErrorCategory;
}

Properties

PropertyTypeDescription
codestringMachine-readable error code. Factory methods set this automatically.
retryablebooleanWhether the AI agent should retry. Defaults to false.
retryAfterMsnumber \| undefinedSuggested delay before retrying, in milliseconds.
categoryErrorCategory \| undefinedError 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=false
throw 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=false
throw 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=true
throw 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=false
throw 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=true
throw 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=false
throw ToolError.internal('Unexpected response format from API');

Factory Summary

FactoryCodeCategoryRetryable
ToolError.auth()AUTH_ERRORauthNo
ToolError.notFound()NOT_FOUNDnot_foundNo
ToolError.rateLimited()RATE_LIMITEDrate_limitYes
ToolError.validation()VALIDATION_ERRORvalidationNo
ToolError.timeout()TIMEOUTtimeoutYes
ToolError.internal()INTERNAL_ERRORinternalNo

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:

  1. Tool handler throws ToolError in the page context
  2. Adapter IIFE serializes the error fields (code, category, retryable, retryAfterMs) into the dispatch response
  3. Chrome extension relays the structured error over WebSocket to the MCP server
  4. 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