Skip to main content

Error Handling

Octavus provides structured error handling across all transports. Errors are categorized by type and source, enabling you to build appropriate UI responses and monitoring.

Error Types

The onError callback receives an OctavusError with structured information:

typescript
import { useOctavusChat, type OctavusError } from '@octavus/react';

const { error, status } = useOctavusChat({
  transport,
  onError: (err: OctavusError) => {
    console.error('Chat error:', {
      type: err.errorType, // Error classification
      message: err.message, // Human-readable message
      source: err.source, // Where the error originated
      retryable: err.retryable, // Can be retried
      retryAfter: err.retryAfter, // Seconds to wait (rate limits)
      code: err.code, // Machine-readable code
      provider: err.provider, // Provider details (if applicable)
    });
  },
});

Error Classification

Error Types

TypeDescriptionTypical Response
rate_limit_errorToo many requestsShow retry timer
quota_exceeded_errorUsage quota exceededShow upgrade prompt
authentication_errorInvalid API keyCheck configuration
permission_errorNo access to resourceCheck permissions
validation_errorInvalid requestFix request data
provider_errorLLM provider issueRetry or show error
provider_overloadedProvider at capacityRetry with backoff
provider_timeoutProvider timed outRetry
tool_errorTool execution failedShow tool error
internal_errorPlatform errorShow generic error

Error Sources

SourceDescription
platformOctavus platform error
providerLLM provider error (OpenAI, Anthropic, etc.)
toolTool execution error
clientClient-side error (network, parsing)

Type Guards

Use type guards to handle specific error types:

typescript
import {
  useOctavusChat,
  isRateLimitError,
  isQuotaExceededError,
  isAuthenticationError,
  isProviderError,
  isToolError,
  isRetryableError,
} from '@octavus/react';

const { error } = useOctavusChat({
  transport,
  onError: (err) => {
    if (isRateLimitError(err)) {
      // Transient rate limit (429) - show a countdown and retry
      showRetryTimer(err.retryAfter ?? 60);
      return;
    }

    if (isQuotaExceededError(err)) {
      // Terminal allowance block (402) - prompt an upgrade, do not retry
      showUpgradePrompt(err.message);
      return;
    }

    if (isAuthenticationError(err)) {
      // Configuration issue - shouldn't happen in production
      reportConfigError(err);
      return;
    }

    if (isProviderError(err)) {
      // LLM service issue
      showProviderError(err.provider?.name ?? 'AI service');
      return;
    }

    if (isToolError(err)) {
      // Tool failed - already shown inline
      return;
    }

    if (isRetryableError(err)) {
      // Generic retryable error
      showRetryButton();
      return;
    }

    // Non-retryable error
    showGenericError(err.message);
  },
});

Provider Error Details

When errors come from LLM providers, additional details are available:

typescript
if (isProviderError(error) && error.provider) {
  console.log({
    name: error.provider.name, // 'anthropic', 'openai', 'google'
    model: error.provider.model, // Model that caused the error
    statusCode: error.provider.statusCode, // HTTP status code
    errorType: error.provider.errorType, // Provider's error type
    requestId: error.provider.requestId, // For support tickets
  });
}

Retrying After Errors

Use retry() to re-execute the last trigger from the same starting point. Messages are rolled back, the user message is re-added (if any), and the agent re-executes. Files are reused without re-uploading.

tsx
const { error, canRetry, retry } = useOctavusChat({ transport });

// Retry after any error
if (canRetry) {
  await retry();
}

retry() also works after stopping (cancellation) or when the result is unsatisfactory - not just errors.

Building Error UI

tsx
import {
  useOctavusChat,
  isRateLimitError,
  isQuotaExceededError,
  isAuthenticationError,
  isProviderError,
} from '@octavus/react';

function Chat() {
  const { error, status, retry, canRetry } = useOctavusChat({ transport });

  return (
    <div>
      {/* Error display */}
      {error && (
        <div className="bg-red-50 border border-red-200 rounded-lg p-4">
          <div className="font-medium text-red-800">{getErrorTitle(error)}</div>
          <p className="text-red-600 text-sm mt-1">{error.message}</p>
          {isRateLimitError(error) && error.retryAfter && (
            <p className="text-red-500 text-sm mt-2">
              Please try again in {error.retryAfter} seconds
            </p>
          )}
          {canRetry && (
            <button className="mt-3 text-red-700 underline" onClick={() => void retry()}>
              Retry
            </button>
          )}
        </div>
      )}
    </div>
  );
}

function getErrorTitle(error: OctavusError): string {
  if (isRateLimitError(error)) return 'Service is busy';
  if (isQuotaExceededError(error)) return 'Usage limit reached';
  if (isAuthenticationError(error)) return 'Configuration error';
  if (isProviderError(error)) return 'AI service unavailable';
  return 'Something went wrong';
}

Monitoring & Logging

Log errors for monitoring and debugging:

typescript
useOctavusChat({
  transport,
  onError: (err) => {
    // Send to your monitoring service
    analytics.track('octavus_error', {
      errorType: err.errorType,
      source: err.source,
      retryable: err.retryable,
      code: err.code,
      provider: err.provider?.name,
    });

    // Log for debugging
    console.error('[Octavus]', {
      type: err.errorType,
      message: err.message,
      source: err.source,
      provider: err.provider,
    });
  },
});

Error State

The hook exposes error state directly:

typescript
const { error, status, retry, canRetry } = useOctavusChat({ transport });

// status is 'error' when an error occurred
// error contains the OctavusError object

// Option 1: Retry the same trigger (rolls back messages, re-executes)
if (canRetry) {
  await retry();
}

// Option 2: Send a new message (clears the error)
await send('user-message', { USER_MESSAGE: 'Try again' });

Rate Limit Handling

Rate limits include retry information:

typescript
if (isRateLimitError(error)) {
  const waitTime = error.retryAfter ?? 60; // Default to 60 seconds

  // Show countdown
  setCountdown(waitTime);
  const timer = setInterval(() => {
    setCountdown((c) => {
      if (c <= 1) {
        clearInterval(timer);
        return 0;
      }
      return c - 1;
    });
  }, 1000);
}

Error Event Structure

For custom transports or direct event handling, errors follow this structure:

typescript
interface ErrorEvent {
  type: 'error';
  errorType: ErrorType;
  message: string;
  source: ErrorSource;
  retryable: boolean;
  retryAfter?: number;
  code?: string;
  provider?: {
    name: string;
    model?: string;
    statusCode?: number;
    errorType?: string;
    requestId?: string;
  };
  tool?: {
    name: string;
    callId?: string;
  };
}

Tool Errors

Tool errors are handled differently - they appear inline on the tool call:

tsx
function ToolCallPart({ part }: { part: UIToolCallPart }) {
  return (
    <div>
      <span>{part.toolName}</span>

      {part.status === 'error' && <div className="text-red-500 text-sm mt-1">{part.error}</div>}
    </div>
  );
}

Tool errors don't trigger onError - they're captured on the tool call part itself.