From 40d5aaf70b635716de2a2ab5d1f1c70e59743de8 Mon Sep 17 00:00:00 2001 From: Eric Gustin Date: Thu, 11 Sep 2025 13:21:14 -0700 Subject: [PATCH 1/5] Update Tool Error Handling --- pages/home/build-tools/handle-tool-errors.mdx | 254 +++++++++++++++--- 1 file changed, 214 insertions(+), 40 deletions(-) diff --git a/pages/home/build-tools/handle-tool-errors.mdx b/pages/home/build-tools/handle-tool-errors.mdx index 7c36b5cc..fb6b352f 100644 --- a/pages/home/build-tools/handle-tool-errors.mdx +++ b/pages/home/build-tools/handle-tool-errors.mdx @@ -1,78 +1,252 @@ --- -title: "Tool Errors" -description: "Documentation for the different types of tool errors in the Arcade Tool SDK" +title: "Tool Error Handling" +description: "Learn how to handle errors when building tools with Arcade's Tool Development Kit (TDK)" --- -# Tool errors in Arcade +# Tool Error Handling -When working with Arcade's Tool SDK, you may encounter various types of errors. This guide will help you understand and handle these errors effectively. +When building tools with Arcade's Tool Development Kit (TDK), understanding error handling is crucial for creating robust and reliable tools. This guide covers everything you need to know about handling errors from a tool developer's perspective. -## Handling errors in your tools +## Error handling philosophy -In most cases, you don't need to raise errors explicitly in your tools. The `@tool` decorator takes care of proper error propagation and handling. When an unexpected error occurs during tool execution, Arcade's `ToolExecutor` and `@tool` decorator will raise a `ToolExecutionError` with the necessary context and traceback information. +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 tools because the `@tool` decorator automatically adapts common exceptions into appropriate Arcade errors. -However, if you want to retry the tool call with additional content to improve the tool call's input parameters, you can raise a [RetryableToolError](/home/build-tools/retry-tools-with-improved-prompt) within the tool. +## Error hierarchy -## Common error scenarios +Arcade uses a structured error hierarchy to categorize different types of errors: + +``` +ToolkitError # (Abstract base class) +├── ToolkitLoadError # Occurs during toolkit 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 -Let's explore some common error scenarios you might encounter: +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 have error adapters, see [arcade_tdk/error_adapters/__init__.py](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/error_adapters/__init__.py) -### 1. Output type mismatch +### Automatic error adaptation -This occurs when the expected output type of a tool does not match the actual output type when executed. +For tools using `httpx` or `requests`, error adaptation happens automatically: + +```python +from typing import Annotated +from arcade_tdk import tool +import httpx -```json -{ - "name": "tool_call_error", - "message": "tool call failed - Example.Hello failed: Failed to - serialize tool output" -} +@tool +def 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() ``` -For example, the following tool will raise the above error because the tool's definition specifies that the output should be a string, but the tool returns a list: +### Explicit error adapters + +For tools using specific SDKs, you can specify error adapters explicitly: ```python +import googleapiclient from typing import Annotated from arcade_tdk import tool +from arcade_tdk.error_adapters import GoogleErrorAdapter + +@tool( + requires_auth=Google(scopes=["https://www.googleapis.com/auth/gmail.readonly"]), + error_adapters=[GoogleErrorAdapter] +) +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 context to improve the tool call's input parameters: + +```python +from typing import Annotated +from arcade_tdk import tool +from 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 context from the user or orchestrator is needed before the tool call can be retried by an LLM: + +```python +from os import path +from typing import Annotated +from arcade_tdk import tool +from arcade_tdk.errors import ContextRequiredToolError @tool -def hello(name: Annotated[str, "The name of the friend to greet"]) -> str: - """ - Says hello to a friend - """ - return ["hello", name] +def 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 ``` -### 2. Input parameter type error +### ToolExecutionError -This occurs when the input parameter of a tool is of an unsupported type. +Use for unrecoverable, but known, errors when you want to provide specific error context: +```python +from typing import Annotated +from arcade_tdk import tool +from arcade_tdk.errors import ToolExecutionError + +@tool +def 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 ``` -Type error encountered while adding tool list_org_repositories from -arcade_github.tools.repositories. Reason: issubclass() arg 1 must be a class + +### UpstreamError + +Use for custom handling of upstream service errors: + +```python +from arcade_tdk import tool +from arcade_tdk.errors import UpstreamError +import httpx + +@tool +def 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 tool has invalid definitions and are caught when the tool is loaded: + +#### Invalid input parameter types + +```python +from arcade_tdk import tool + +@tool +def 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 + +@tool +def invalid_tool(name: str): # ❌ Missing return type + """This will raise a ToolOutputSchemaError.""" + return f"Hello {name}" ``` -For example, the following tool will raise the above error because the tool input parameter is of an unsupported type: +#### Invalid parameter annotations ```python from typing import Annotated from arcade_tdk import tool @tool -def hello(names: Annotated[tuple[str, str, str], "The names of the friends to greet"]) -> str: - """ - Says hello to a list of friends - """ - return f"Hello, {names[0]}, {names[1]}, and {names[2]}!" +def invalid_tool(name: Annotated[str, "desc1", "desc2", "extra"]) -> str: # ❌ Too many annotations + """This will raise a ToolInputSchemaError.""" + return f"Hello {name}" ``` -### 3. Unexpected HTTP error during tool execution +### Runtime errors -This occurs when the tool makes an HTTP request and receives a non-2xx response. Specifically for the example below, the authenticated user did not have permission to access the private organization's repositories. +These errors occur during tool execution: -```json -{ - "name": "tool_call_error", - "message": "tool call failed - Github.ListOrgRepositories failed: Error accessing 'https://api.github.com/orgs/a-private-org/repos': Failed to process request. Status code: 401" -} +#### Output type mismatch + +```python +from typing import Annotated +from arcade_tdk import tool + +@tool +def 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. + +## Best practices + +1. **Let Arcade handle most errors**: There's no need to wrap your tool logic in try/catch blocks unless you need custom error handling. + +2. **Use specific error types**: When you do need to raise errors explicitly, use the most specific error type available. + +3. **Include additional context**: For `RetryableToolError` and `ContextRequiredToolError`, use the `additional_prompt_content` parameter to guide the LLM or user. + From e68081ca98a40235b36af6bf0183646c33a5ea33 Mon Sep 17 00:00:00 2001 From: Eric Gustin Date: Mon, 15 Sep 2025 11:57:04 -0700 Subject: [PATCH 2/5] Show client lib usage --- pages/home/build-tools/handle-tool-errors.mdx | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/pages/home/build-tools/handle-tool-errors.mdx b/pages/home/build-tools/handle-tool-errors.mdx index fb6b352f..fe311226 100644 --- a/pages/home/build-tools/handle-tool-errors.mdx +++ b/pages/home/build-tools/handle-tool-errors.mdx @@ -3,6 +3,8 @@ title: "Tool Error Handling" description: "Learn how to handle errors when building tools with Arcade's Tool Development Kit (TDK)" --- +import { Tabs } from "nextra/components"; + # Tool Error Handling When building tools with Arcade's Tool Development Kit (TDK), understanding error handling is crucial for creating robust and reliable tools. This guide covers everything you need to know about handling errors from a tool developer's perspective. @@ -242,6 +244,155 @@ def invalid_output(name: Annotated[str, "Name to greet"]) -> str: 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: + + + +```python +""" +This example demonstrates how to handle different kinds of output errors when executing a tool. +""" + +from arcadepy import Arcade # pip install arcadepy +from arcadepy.types.execute_tool_response import OutputError + + +# Requires arcadepy >= 1.8.0 +def 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 variable +user_id = "{arcade_user_id}" +tool_name = "Reddit.GetPostsInSubreddit" +tool_input = {"subreddit": "programming", "limit": 1} + +# Go through the OAuth flow for the tool +auth_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 tool +response = 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 +/** + * 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.0 +function 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 variable +const userId = "{arcade_user_id}"; +const toolName = "Reddit.GetPostsInSubreddit"; +const toolInput = { subreddit: "programming", limit: 1 }; + +// Go through the OAuth flow for the tool +const 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 tool +const 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); +} +``` + + + +### Error types in client libraries + +To see the full structure of an OutputError, see [arcade-py OutputError](https://github.com/ArcadeAI/arcade-py/blob/942eb2cf41bc14b6c82f0e4acd8b11ef1978cb8d/src/arcadepy/types/execute_tool_response.py#L12) and [arcade-js OutputError](https://github.com/ArcadeAI/arcade-js/blob/902ef0ce9ff0412ca0d66a862cb4301759d3f87f/src/resources/tools/tools.ts#L166). + ## Best practices 1. **Let Arcade handle most errors**: There's no need to wrap your tool logic in try/catch blocks unless you need custom error handling. From 5f34885765605555952e66b181e233e858fa2512 Mon Sep 17 00:00:00 2001 From: Eric Gustin <34000337+EricGustin@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:39:56 -0700 Subject: [PATCH 3/5] Update pages/home/build-tools/handle-tool-errors.mdx Co-authored-by: Evan Tahler --- pages/home/build-tools/handle-tool-errors.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/home/build-tools/handle-tool-errors.mdx b/pages/home/build-tools/handle-tool-errors.mdx index fe311226..99345585 100644 --- a/pages/home/build-tools/handle-tool-errors.mdx +++ b/pages/home/build-tools/handle-tool-errors.mdx @@ -72,7 +72,7 @@ from arcade_tdk.error_adapters import GoogleErrorAdapter @tool( requires_auth=Google(scopes=["https://www.googleapis.com/auth/gmail.readonly"]), - error_adapters=[GoogleErrorAdapter] + error_adapters=[GoogleErrorAdapter] # note the tool opts-into the error adapter ) def send_email( num_emails: Annotated[int, "The number of emails to send"], From e793a116dc540b208dcf831a230fd680404f003f Mon Sep 17 00:00:00 2001 From: Eric Gustin Date: Mon, 15 Sep 2025 12:43:08 -0700 Subject: [PATCH 4/5] Link to error adapter implementation example --- pages/home/build-tools/handle-tool-errors.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/home/build-tools/handle-tool-errors.mdx b/pages/home/build-tools/handle-tool-errors.mdx index 99345585..ad519bb9 100644 --- a/pages/home/build-tools/handle-tool-errors.mdx +++ b/pages/home/build-tools/handle-tool-errors.mdx @@ -38,7 +38,7 @@ ToolkitError # (Abstract base class) ## 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 have error adapters, see [arcade_tdk/error_adapters/__init__.py](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/error_adapters/__init__.py) +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 have error adapters, see [arcade_tdk/error_adapters/__init__.py](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/error_adapters/__init__.py). You may want to create your own error adapter or contribute an error adapter to the TDK. If so, see the [HTTP Error Adapter](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/providers/http/error_adapter.py) for an example. ### Automatic error adaptation @@ -72,7 +72,7 @@ from 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 + error_adapters=[GoogleErrorAdapter] # note the tool opts-into the error adapter ) def send_email( num_emails: Annotated[int, "The number of emails to send"], From 99c35ee1b266e0b496fd9aa5cdfeb11e73981280 Mon Sep 17 00:00:00 2001 From: Eric Gustin Date: Mon, 15 Sep 2025 12:45:21 -0700 Subject: [PATCH 5/5] Link to error adapter protocol: --- pages/home/build-tools/handle-tool-errors.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/home/build-tools/handle-tool-errors.mdx b/pages/home/build-tools/handle-tool-errors.mdx index ad519bb9..7b98280f 100644 --- a/pages/home/build-tools/handle-tool-errors.mdx +++ b/pages/home/build-tools/handle-tool-errors.mdx @@ -38,7 +38,7 @@ ToolkitError # (Abstract base class) ## 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 have error adapters, see [arcade_tdk/error_adapters/__init__.py](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/error_adapters/__init__.py). You may want to create your own error adapter or contribute an error adapter to the TDK. If so, see the [HTTP Error Adapter](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/providers/http/error_adapter.py) for an example. +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](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/error_adapters/__init__.py). You may want to create your own error adapter or contribute an error adapter to the TDK. If so, see the [HTTP Error Adapter](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/providers/http/error_adapter.py) for an example. Ensure your error adapter implements the [ErrorAdapter protocol](https://github.com/ArcadeAI/arcade-ai/blob/main/libs/arcade-tdk/arcade_tdk/error_adapters/base.py). ### Automatic error adaptation