When building tools with Arcade’s Tool Development Kit (), understanding error handling is crucial for creating robust and reliable tools. This guide covers everything you need to know about handling errors from a developer’s perspective.
Error handling philosophy
Arcade’s error handling is designed to minimize boilerplate code while providing rich error information. In most cases, you don’t need to explicitly handle errors in your because the @tool decorator automatically adapts common exceptions into appropriate Arcade errors.
Error hierarchy
Arcade uses a structured error hierarchy to categorize different types of errors:
PLAINTEXT
ToolkitError # (Abstract base class)├── ToolkitLoadError # Occurs during MCP Server import└── ToolError # (Abstract) ├── ToolDefinitionError # Detected when tool is added to catalog │ ├── ToolInputSchemaError # Invalid input parameter types/annotations │ └── ToolOutputSchemaError # Invalid return type annotations └── ToolRuntimeError # Errors during tool execution ├── ToolSerializationError # (Abstract) │ ├── ToolInputError # JSON to Python conversion fails │ └── ToolOutputError # Python to JSON conversion fails └── ToolExecutionError # Errors during tool execution ├── RetryableToolError # Tool can be retried with extra context ├── ContextRequiredToolError # Additional context needed before retry ├── FatalToolError # Unhandled bugs in the tool implementation └── UpstreamError # HTTP/API errors from external services └── UpstreamRateLimitError # Rate limiting errors from external services
Error adapters
Error adapters automatically translate common exceptions (from httpx, requests, SDKs, etc.) into appropriate Arcade errors. This means zero boilerplate error handling code for you. To see which SDKs already have error adapters, see arcade_tdk/error_adapters/init.py. You may want to create your own error adapter or contribute an error adapter to the . If so, see the HTTP Error Adapter for an example. Ensure your error adapter implements the ErrorAdapter protocol .
Automatic error adaptation
For using httpx or requests, error adaptation happens automatically:
Python
from typing import Annotatedfrom arcade_tdk import toolimport httpx@tooldef fetch_data( url: Annotated[str, "The URL to fetch data from"],) -> Annotated[dict, "The data fetched from the API endpoint"]: """Fetch data from an API endpoint.""" # No need to wrap in try/catch - Arcade handles HTTP errors automatically response = httpx.get(url) response.raise_for_status() # This will be adapted to UpstreamError if it raises return response.json()
Explicit error adapters
For using specific SDKs, you can specify error adapters explicitly:
Python
import googleapiclientfrom typing import Annotatedfrom arcade_tdk import toolfrom arcade_tdk.error_adapters import GoogleErrorAdapter@tool( requires_auth=Google(scopes=["https://www.googleapis.com/auth/gmail.readonly"]), error_adapters=[GoogleErrorAdapter] # note the tool opts-into the error adapter)def send_email( num_emails: Annotated[int, "The number of emails to send"],) -> Annotated[dict, "The emails sent using the Gmail API"]: """Send an email using the Gmail API.""" # Google API Client errors will be automatically adapted to Upstream Arcade errors for you service = _build_gmail_service(context) emails = service.users.messages().get( userId="me", id=num_emails ).execute() # This will be adapted to UpstreamError if it raises parsed_emails = _parse_emails(emails) return parsed_emails
When to raise errors explicitly
While Arcade handles most errors automatically, there are specific cases where you should raise errors explicitly:
RetryableToolError
Use when the LLM can retry the tool call with more to improve the call’s input parameters:
Python
from typing import Annotatedfrom arcade_tdk import toolfrom arcade_tdk.errors import RetryableToolError@tool(requires_auth=Reddit(scopes=["read"]))def search_posts( subreddit: Annotated[str, "The subreddit to search in"], query: Annotated[str, "The query to search for"],) -> Annotated[list[dict], "The posts found in the subreddit"]: """Search for posts in a subreddit.""" if is_invalid_subreddit(subreddit): # additional_prompt_content should be provided back to the LLM raise RetryableToolError( "Please specify a subreddit name, such as 'python' or 'programming'", additional_prompt_content=f"{subreddit} is an invalid subreddit name. Please specify a valid subreddit name" ) # ... rest of implementation
ContextRequiredToolError
Use when additional from the user or orchestrator is needed before the call can be retried by an LLM:
Python
from os import pathfrom typing import Annotatedfrom arcade_tdk import toolfrom arcade_tdk.errors import ContextRequiredToolError@tooldef delete_file(filename: Annotated[str, "The filename to delete"]) -> Annotated[str, "The filename that was deleted"]: """Delete a file from the system.""" if not os.path.exists(filename): raise ContextRequiredToolError( "File with provided filename does not exist", additional_prompt_content=f"{filename} does not exist. Did you mean one of these: {get_valid_filenames()}", ) # ... deletion logic
ToolExecutionError
Use for unrecoverable, but known, errors when you want to provide specific error :
Python
from typing import Annotatedfrom arcade_tdk import toolfrom arcade_tdk.errors import ToolExecutionError@tooldef process_data(data_id: Annotated[str, "The ID of the data to process"]) -> Annotated[dict, "The processed data"]: """Process data by ID.""" try: data = get_data_from_database(data_id) except Exception as e: raise ToolExecutionError("Database connection failed.") from e # ... processing logic
UpstreamError
Use for custom handling of upstream service errors:
Python
from arcade_tdk import toolfrom arcade_tdk.errors import UpstreamErrorimport httpx@tooldef create_issue(title: str, description: str) -> dict: """Create a GitHub issue.""" try: response = httpx.post("/repos/owner/repo/issues", json={ "title": title, "body": description }) response.raise_for_status() except httpx.HTTPStatusError as e: if e.response.status_code == 422: raise UpstreamError( "Invalid issue data provided. Check title and description.", status_code=422 ) from e # Let other HTTP errors be handled automatically raise return response.json()
Common error scenarios
Tool definition errors
These errors occur when your has invalid definitions and are caught when the tool is loaded:
Invalid input parameter types
Python
from arcade_tdk import tool@tooldef invalid_tool(data: tuple[str, str, str]) -> str: # ❌ Tuples not supported """This will raise a ToolInputSchemaError.""" return f"Hello {data[0]}"
Missing return type annotation
Python
from arcade_tdk import tool@tooldef invalid_tool(name: str): # ❌ Missing return type """This will raise a ToolOutputSchemaError.""" return f"Hello {name}"
Invalid parameter annotations
Python
from typing import Annotatedfrom arcade_tdk import tool@tooldef invalid_tool(name: Annotated[str, "desc1", "desc2", "extra"]) -> str: # ❌ Too many annotations """This will raise a ToolInputSchemaError.""" return f"Hello {name}"
Runtime errors
These errors occur during :
Output type mismatch
Python
from typing import Annotatedfrom arcade_tdk import tool@tooldef invalid_output(name: Annotated[str, "Name to greet"]) -> str: """Says hello to a friend.""" return ["hello", name] # ❌ Returns list instead of string
This will raise a ToolOutputError because the return type doesn’t match the annotation.
Handling tool errors in client libraries
When using Arcade’s client libraries to execute tools, you may encounter various types of errors returned by the tools. The client libraries provide structured error information that helps you handle different error scenarios appropriately.
Client error handling examples
Here’s how to handle different types of output errors when executing tools with Arcade’s client libraries:
PythonJavaScript
Python
Python
"""This example demonstrates how to handle different kinds of output errors when executing a tool."""from arcadepy import Arcade # pip install arcadepyfrom arcadepy.types.execute_tool_response import OutputError# Requires arcadepy >= 1.8.0def handle_tool_error(error: OutputError) -> None: """Example of how to identify different kinds of output errors.""" error_kind = error.kind if error_kind == OutputError.Kind.TOOL_RUNTIME_BAD_INPUT_VALUE: # You provided the executed tool with an invalid input value print(error.message) elif error_kind == OutputError.Kind.TOOL_RUNTIME_RETRY: # The tool returned a retryable error. Provide the additional # prompt content to the LLM and retry the tool call instructions_for_llm = error.additional_prompt_content print(instructions_for_llm) elif error_kind == OutputError.Kind.TOOL_RUNTIME_CONTEXT_REQUIRED: # The tool requires extra context from the user or orchestrator. # Provide the additional prompt content to them and then retry the # tool call with the new context request_for_context = error.additional_prompt_content print(request_for_context) elif error_kind == OutputError.Kind.TOOL_RUNTIME_FATAL: # The tool encountered a fatal error during execution print(error.message) elif error_kind == OutputError.Kind.UPSTREAM_RUNTIME_RATE_LIMIT: # The tool encountered a rate limit error from an upstream service # Wait for the specified amount of time and then retry the tool call seconds_to_wait = error.retry_after_ms / 1000 print(f"Wait for {seconds_to_wait} seconds before retrying the tool call") elif error_kind.startswith("UPSTREAM_"): # The tool encountered an error from an upstream service print(error.message)client = Arcade() # Automatically finds the `ARCADE_API_KEY` env variableuser_id = "{arcade_user_id}"tool_name = "Reddit.GetPostsInSubreddit"tool_input = {"subreddit": "programming", "limit": 1}# Go through the OAuth flow for the toolauth_response = client.tools.authorize( tool_name=tool_name, user_id=user_id,)if auth_response.status != "completed": print(f"Click this link to authorize: {auth_response.url}")client.auth.wait_for_completion(auth_response)# Execute the toolresponse = client.tools.execute( tool_name=tool_name, input=tool_input, user_id=user_id, include_error_stacktrace=True,)if response.output.error: handle_tool_error(response.output.error)
JavaScript
JavaScript
/** * This example demonstrates how to handle different kinds of output errors when executing a tool. */import { Arcade } from "@arcadeai/arcadejs"; // npm install @arcadeai/arcadejs// Requires @arcadeai/arcadejs >= 1.10.0function handleToolError(error) { const errorKind = error.kind; if (errorKind === "TOOL_RUNTIME_BAD_INPUT_VALUE") { // You provided the executed tool with an invalid input value console.log(error.message); } else if (errorKind === "TOOL_RUNTIME_RETRY") { // The tool returned a retryable error. Provide the additional // prompt content to the LLM and retry the tool call const instructionsForLLM = error.additional_prompt_content; console.log(instructionsForLLM); } else if (errorKind === "TOOL_RUNTIME_CONTEXT_REQUIRED") { // The tool requires extra context from the user or orchestrator. // Provide the additional prompt content to them and then retry the // tool call with the new context const requestForContext = error.additional_prompt_content; console.log(requestForContext); } else if (errorKind === "TOOL_RUNTIME_FATAL") { // The tool encountered a fatal error during execution console.log(error.message); } else if (errorKind === "UPSTREAM_RUNTIME_RATE_LIMIT") { // The tool encountered a rate limit error from an upstream service // Wait for the specified amount of time and then retry the tool call const secondsToWait = error.retry_after_ms / 1000; console.log(`Wait for ${secondsToWait} seconds before retrying the tool call`); } else if (errorKind.startsWith("UPSTREAM_")) { // The tool encountered an error from an upstream service console.log(error.message); }}const client = new Arcade(); // Automatically finds the `ARCADE_API_KEY` env variableconst userId = "{arcade_user_id}";const toolName = "Reddit.GetPostsInSubreddit";const toolInput = { subreddit: "programming", limit: 1 };// Go through the OAuth flow for the toolconst authResponse = await client.tools.authorize({ tool_name: toolName, user_id: userId,});if (authResponse.status !== "completed") { console.log(`Click this link to authorize: ${authResponse.url}`);}await client.auth.waitForCompletion(authResponse);// Execute the toolconst response = await client.tools.execute({ tool_name: toolName, input: toolInput, user_id: userId, include_error_stacktrace: true,});if (response.output.error) { handleToolError(response.output.error);}