If you're building with Claude's tool use API, you've probably run into the "tool result could not be submitted" error at some point. It's one of those frustrating errors where the message alone doesn't tell you much about what actually went wrong.
I've hit this error more times than I'd like to admit while implementing tool use in my own projects. After working through the patterns, here's a practical breakdown of the causes and fixes.
When Does This Error Appear?
"Tool result could not be submitted" appears during the tool use flow, which works like this:
- A user sends a message
- Claude decides to call a tool and returns a response containing a
tool_useblock - Your code executes the tool and retrieves a result
- You send that result back as a
tool_resultin the next message
When step four fails, you get this error. The message might suggest the problem is on Claude's end, but in most cases it's an implementation issue you can fix.
Common Causes and Fixes
1. Tool Result Is Too Large (Most Common)
This is the most frequent culprit. If you pass raw HTML from a web scraper, a large database dump, or any unfiltered external data as your tool result, you can easily exceed Claude's context window.
Most Claude models support up to roughly 200,000 tokens of context. When the total conversation — including tool results — approaches that limit, the API can't accept new submissions.
The fix: Pre-process your tool results and return only the information Claude actually needs.
# ❌ Returning raw HTML (dangerous)
def search_web(query: str) -> str:
response = requests.get(f"https://example.com/search?q={query}")
return response.text # Could be tens of thousands of characters
# ✅ Extracting only what matters (correct)
def search_web(query: str) -> str:
response = requests.get(f"https://example.com/search?q={query}")
soup = BeautifulSoup(response.text, 'html.parser')
results = []
for item in soup.select('.result')[:5]: # Top 5 results only
title = item.select_one('h3')
snippet = item.select_one('.snippet')
if title and snippet:
results.append(f"Title: {title.text}\nSummary: {snippet.text[:200]}")
return "\n\n".join(results) if results else "No results found"A good rule of thumb: keep each individual tool result under 5,000–10,000 tokens.
2. Mismatched tool_use_id
Claude's tool_use block includes an id field. Your tool_result must reference that exact ID in tool_use_id. Any mismatch will cause the submission to fail.
response = anthropic.messages.create(...)
# ❌ Hardcoding or guessing the ID
tool_results = [{
"type": "tool_result",
"tool_use_id": "toolu_01", # This will fail unless it matches exactly
"content": result_content
}]
# ✅ Using the ID from Claude's actual response
for block in response.content:
if block.type == "tool_use":
tool_result = {
"type": "tool_result",
"tool_use_id": block.id, # ← Use the ID Claude gave you
"content": str(tool_output)
}This becomes especially tricky when Claude calls multiple tools in parallel — make sure you're pairing each result with the correct ID.
3. Wrong content Type
The content field in a tool_result must be a string or an array (for multi-modal content including images). Passing None, integers, or other types will cause issues.
# ❌ Passing non-string types
"content": None
"content": 42
# ✅ Always convert to string
"content": str(result) if result is not None else "No result returned from tool"When a tool execution fails, use is_error: true instead of an empty string:
{
"type": "tool_result",
"tool_use_id": block.id,
"content": "Error: Database connection timed out after 30 seconds",
"is_error": True # Claude will recognize this and try to recover
}4. Network or Timeout Issues
Long-running tools can cause the underlying HTTP connection to time out before you can submit the result. This is common with tools that call external APIs or process large files.
import anthropic
import httpx
# Set a longer timeout explicitly
client = anthropic.Anthropic(
http_client=httpx.Client(timeout=60.0) # 60 seconds
)For tools that genuinely take a long time, consider breaking the work into smaller chunks or using asynchronous processing.
5. Malformed Message History
As conversations grow, it's easy for the messages array structure to become inconsistent. Tool use has strict requirements about message ordering and format.
# Correct message structure for tool use
messages = [
# 1. User message
{"role": "user", "content": "What's the weather in Tokyo?"},
# 2. Claude's tool call response
{"role": "assistant", "content": [
{"type": "text", "text": "Let me check the weather for you."},
{"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {"city": "Tokyo"}}
]},
# 3. Tool result (sent as "user" role)
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_01", "content": "Tokyo: Sunny, 25°C"}
]},
# 4. Continue the conversation...
]Every assistant message that includes a tool_use block must be followed by a user message with the corresponding tool_result before you can continue the conversation.
Debugging Checklist
When you hit this error, work through these checks in order:
Verify the tool_use_id match first — print both the ID from Claude's response and the ID in your tool_result and confirm they're identical. Then check the result size by printing len(result_content) to see if you're returning unexpectedly large data. After that, check the content type with type(result_content) and ensure it's a str. Finally, inspect the message array structure to confirm roles alternate correctly and every tool call has a corresponding result.
A Note on Streaming
If you're using the streaming API, the same root causes apply, but tracking them is harder because the response comes in chunks. I'd recommend debugging in non-streaming mode first to confirm the tool use logic is correct, then switching to streaming once everything works.
Wrapping Up
The "tool result could not be submitted" error almost always comes down to one of these five patterns. Oversized results are the most surprising one — code that works fine in development can suddenly fail in production when it encounters real-world data volumes.
Adding explicit size limits to your tool implementations early in development will save you significant debugging time later.