●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Let Claude Diagnose Its Own Tool Errors — Building a Self-Correction Loop with the Anthropic API
Learn how to handle Tool Use failures gracefully by feeding error details back to Claude using the is_error flag, enabling self-diagnosis and automatic retry. Includes working Python code and production antipatterns to avoid.
If you've spent any time building with Claude's Tool Use API, you've hit this problem: a tool fails, your agent loop crashes, and the user sees an error that had nothing to do with Claude's reasoning — just a transient network hiccup or a malformed input parameter.
The standard fix is a try-except with a hardcoded retry. But there's a better approach: let Claude itself diagnose the error and decide how to fix it. Once I switched to this pattern, the reliability of my agent implementations improved noticeably.
Assume Tools Will Fail
Most implementations treat tool execution as if it will always succeed:
# Common pattern — no error handlingresult = execute_tool(tool_name, tool_input)messages.append({ "role": "user", "content": [{"type": "tool_result", "tool_use_id": id, "content": str(result)}]})
When execute_tool raises an exception, the entire agent loop stops. A temporary 503 from an external API becomes a failed request for the user.
In production, tool failure rates are higher than you'd expect. Agents that integrate external APIs typically see 0.5–2% failure rates per month. At scale, that's not acceptable to silently crash.
The is_error Flag — Giving Claude Feedback
The Anthropic API's tool_result message type has an is_error field. Set it to true, and Claude understands that the tool call failed.
What you put in content matters enormously. A bare "Error: 500" gives Claude nothing to work with. But a message that explains what failed, what input was used, and what might fix it gives Claude enough context to self-correct.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦How to classify tool failures into input, transient, and structural errors and route each to self-correction or an immediate abort
✦Avoiding 400s by pairing tool_use with tool_result, plus keeping conversation history from bloating during error loops
✦Choosing exit conditions and retry strategies for prototypes, user-facing production, and batch pipelines
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Here's a complete implementation that feeds error details back to Claude and allows up to three self-correction attempts:
import anthropicimport jsonclient = anthropic.Anthropic()tools = [ { "name": "search_database", "description": "Search the database by keyword. 'query' must be a non-empty string.", "input_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Search query (required, must not be empty)" }, "limit": { "type": "integer", "description": "Number of results to return (default: 10, max: 100)", "default": 10 } }, "required": ["query"] } }]def execute_tool(tool_name: str, tool_input: dict) -> dict: """Execute a tool. Raises exceptions with actionable error messages.""" if tool_name == "search_database": query = tool_input.get("query", "") if not query: raise ValueError( "The 'query' parameter is empty. Please provide a non-empty search term." ) limit = tool_input.get("limit", 10) if limit > 100: raise ValueError( f"'limit' must be 100 or less, but received {limit}. Please reduce the value." ) return { "results": [ {"id": i, "title": f"Result {i} for '{query}'"} for i in range(1, min(limit, 5) + 1) ], "total": min(limit, 5), } raise ValueError(f"Unknown tool: {tool_name}")def run_with_self_correction(user_message: str, max_attempts: int = 3) -> str: """ Run a Tool Use loop with self-correction. On tool failure, feed error details back to Claude and allow retry. """ messages = [{"role": "user", "content": user_message}] for attempt in range(max_attempts): response = client.messages.create( model="claude-opus-4-6", max_tokens=1024, tools=tools, messages=messages, ) if response.stop_reason == "end_turn": for block in response.content: if hasattr(block, "text"): return block.text return "" if response.stop_reason != "tool_use": break messages.append({"role": "assistant", "content": response.content}) tool_results = [] for block in response.content: if block.type != "tool_use": continue try: result = execute_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result), }) except Exception as e: error_content = ( f"Tool '{block.name}' failed (attempt {attempt + 1}/{max_attempts}).\n" f"Input used: {json.dumps(block.input)}\n" f"Error: {type(e).__name__}: {str(e)}\n" f"Remaining attempts: {max_attempts - attempt - 1}. " f"Please correct the input and retry, or respond without the tool." ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "is_error": True, "content": error_content, }) messages.append({"role": "user", "content": tool_results}) return "Maximum retry attempts reached. Tool execution failed repeatedly."if __name__ == "__main__": result = run_with_self_correction( "Search for information about AI agents and show me the top 3 results." ) print(result)
When you run this, you'll see Claude acknowledge the error, adjust its inputs, and try again — often successfully.
What Makes Error Messages Effective
The quality of your error content determines whether self-correction actually works. Claude can't execute code or inspect your system — it can only reason from what you tell it.
Poor error messages:
"Internal Server Error"
"Error 422"
"Validation failed"
Effective error messages include what failed, what value was used, and how to fix it:
"'start_date' format is invalid. Received: '2026-5-1'. Expected format: 'YYYY-MM-DD' (e.g., '2026-05-01'). Please correct and retry."
"'query' is required but received an empty string. Provide a specific search term."
Adding actionable hints to your error messages can push self-correction success rates from around 60% to 85% after the first failure.
Avoiding the Infinite Loop Trap
The biggest risk with self-correction loops is an exit condition that's too loose. These situations can cause infinite loops:
The error is structurally unfixable (server is down, auth token is invalid)
The error message is too vague for Claude to know what to change
The tool definition (description, schema) doesn't accurately reflect what the tool accepts
Combining a consecutive error counter with exponential backoff handles both transient failures and permanent ones:
import timedef run_with_backoff(user_message: str, max_attempts: int = 3) -> str: messages = [{"role": "user", "content": user_message}] consecutive_errors = 0 for attempt in range(max_attempts * 2): if consecutive_errors >= max_attempts: return f"Tool failed {max_attempts} times in a row. Aborting." response = client.messages.create( model="claude-opus-4-6", max_tokens=1024, tools=tools, messages=messages, ) if response.stop_reason == "end_turn": for block in response.content: if hasattr(block, "text"): return block.text return "" if response.stop_reason != "tool_use": break messages.append({"role": "assistant", "content": response.content}) tool_results = [] for block in response.content: if block.type != "tool_use": continue try: result = execute_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result), }) consecutive_errors = 0 # Reset on success except Exception as e: consecutive_errors += 1 wait_secs = min(2 ** (consecutive_errors - 1), 30) time.sleep(wait_secs) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "is_error": True, "content": f"Error (attempt {consecutive_errors}): {str(e)}", }) messages.append({"role": "user", "content": tool_results}) return "Reached processing limit."
When Claude Chooses to Give Up
A well-implemented self-correction loop produces another useful behavior: Claude deciding to respond without the tool when it can't fix the issue. You might see something like "I was unable to retrieve the data after several attempts. The database service appears to be unavailable."
This is correct agent behavior, not a bug. When stop_reason is end_turn and the response text mentions failure, your application layer should treat it as a soft error and alert accordingly — rather than silently returning an empty result.
The Numbers Behind Self-Correction — Cost vs. Benefit
Self-correction isn't a cure-all. Before adding it, it helps to have an intuition for which errors Claude can fix and which it can't — otherwise you'll spend tokens chasing failures that were never recoverable.
As an indie developer, running Tool Use across my own app backends and a pipeline that auto-updates several blogs, I've found that the type of failure changes how well self-correction works. Rough guidance:
The easy thing to miss is that the error feedback itself has a cost. Every is_error exchange is appended to the conversation history, so the longer errors persist, the more input tokens you burn and the higher your latency climbs. Throwing a structural error back at Claude repeatedly is the worst trade of all: it won't get fixed, and you pay in both tokens and latency for the privilege.
Classify the Error Before You Handle It
What the numbers tell us is that you shouldn't feed every error back to Claude uniformly. Classify first, then route by how fixable the error actually is.
from enum import Enumclass ErrorClass(Enum): INPUT = "input" # Fixable by adjusting input -> send back to Claude TRANSIENT = "transient" # Recoverable by waiting -> back off, same input PERMANENT = "permanent" # Not self-fixable -> abortdef classify_error(exc: Exception) -> ErrorClass: name = type(exc).__name__ msg = str(exc).lower() if name in ("ValueError", "KeyError", "ValidationError", "TypeError"): return ErrorClass.INPUT if any(k in msg for k in ("timeout", "503", "502", "temporarily", "rate limit", "429")): return ErrorClass.TRANSIENT if any(k in msg for k in ("401", "403", "auth", "permission", "forbidden", "404", "not found")): return ErrorClass.PERMANENT return ErrorClass.TRANSIENT # treat the unknown conservatively as transient
Once you have the class, the handling falls out naturally. Input errors go back to Claude with a correction hint; transient errors back off and retry the same input; structural errors abort without spending a Claude turn.
def handle_failure(exc, block, attempt, max_attempts): """Return a tool_result (or abort) based on the error class.""" kind = classify_error(exc) if kind == ErrorClass.PERMANENT: # Sending this to Claude won't help. Abort and surface it upstream. raise AbortToolLoop(f"Unrecoverable error: {type(exc).__name__}: {exc}") if kind == ErrorClass.TRANSIENT: wait = min(2 ** attempt, 30) time.sleep(wait) # Keep the input unchanged; just signal it was transient. hint = "The external service is temporarily unavailable. Wait briefly and retry with the same input." else: # INPUT hint = "Review and correct the input. See each parameter's description for the expected format." return { "type": "tool_result", "tool_use_id": block.id, "is_error": True, "content": ( f"Tool '{block.name}' failed (attempt {attempt + 1}/{max_attempts}).\n" f"Input used: {json.dumps(block.input)}\n" f"Error: {type(exc).__name__}: {exc}\n{hint}" ), }
That single routing step prevents the worst case — Claude endlessly mutating its input against an expired auth token it could never have fixed.
What the Docs Don't Tell You
After running this in production, a few things stood out that aren't obvious from the documentation alone.
First, even when you return is_error: True, you must keep the preceding assistant tool_use block in messages. Return a tool_result without its matching tool_use and the API responds with a 400. It's an easy invariant to break when you're focused on error handling.
Second, when a single response contains multiple parallel tool_use blocks, you must return a tool_result for everytool_use_id, or the next request fails with a 400. Even if only some calls failed, return both the successes and the errors.
Third, error exchanges reliably bloat the conversation history. In long loops, resolved older errors crowd out context and eat into max_tokens. Summarizing and collapsing settled error exchanges keeps later turns stable.
Fourth, Claude takes cues from the language of the error text. For a Japanese-facing app, writing error messages in Japanese yields more consistent Japanese correction suggestions. A small detail, but it shows up in the user-facing experience.
Recommendations by Scenario
How far you build this out should depend on the use case.
For prototypes and internal tools, the simple max_attempts=3 self-correction from the start of this article is plenty — no need for the full classification layer.
For user-facing production systems, combine error classification, exponential backoff, a consecutive-error counter, and alerting. Not bleeding money on unfixable errors maps directly to your operating cost.
For batch workloads like automated pipelines, leaning too hard on self-correction is actually less stable. In my own auto-update pipeline, instead of asking Claude to fix things repeatedly, I shunt failed jobs into a holding queue, isolate the cause, and re-submit. As long as the work is idempotent, re-submission won't double-execute. Self-correction shines when you want to resolve things within a single conversation; for unattended, high-volume runs, parking and replaying failures is the more dependable pattern.
Where to Go From Here
Start by adding is_error: True feedback to your existing try-except blocks — that alone makes a meaningful difference. From there, improve the error message content to include actionable correction hints.
For more complex agent patterns, combining this self-correction approach with Claude's Extended Thinking allows deeper reasoning about why a tool failure occurred — worth exploring once the basics are working well.
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.