Guide · Errors and retries
Agent tool error handling: a field guide for web fetch
Which web fetch failures to retry in code, which to hand to the model, and how to word the error so the agent picks a sensible next step instead of looping.
Guide
This article was drafted with AI assistance from the sources listed below and edited and published by Teneo Labs. Disclosure: this site is run by Teneo Labs, the company behind Live Web Data API, which this article mentions.
| Criterion | Your code | The model |
|---|---|---|
| 429 rate limited | Wait for Retry-After, then retry | Sees it only if retries run out |
| Timeout | Retry once or twice with backoff | Picks another source if it persists |
| Not found (404/410) | No retry | Drops the URL or searches again |
| Needs JavaScript | No retry | Tries another page or tells the user |
| Restricted target | No retry | Moves on, does not try variants |
| Auth or out of credits | Stop the task, alert a human | Never sees it |
A web fetch fails more often than most tools an agent calls. Pages move, servers are slow, rate limits kick in, and some pages show nothing without JavaScript. None of this is unusual. What decides whether the agent copes is who handles each failure and what the model is told. This guide gives a taxonomy, a default split between code and model, and wording that works.
Two kinds of failure
Every failure falls into one of two groups.
- Transient. The same call may succeed a moment later: rate limits (429), timeouts, server errors (5xx), dropped connections.
- Permanent for this URL. Repeating the call returns the same answer: not found, gone, needs JavaScript, unsupported content type, a target the provider does not fetch.
A third group sits outside the loop: your own account problems, such as an invalid key or no credits left. The model cannot fix those, so it should never see them.
Retry in code, not in the prompt
When a model sees a 429, it usually calls the tool again at once. That is the worst possible reaction: it adds load, earns another 429 and spends tokens on every round. Retries of transient errors are mechanical, so do them in code:
- Retry only transient errors.
- Use exponential backoff with jitter, as the Amazon Builders' Library article describes, so parallel calls do not retry in lockstep.
- If the response has a
Retry-Afterheader, wait at least that long. MDN documents it as either a number of seconds or an HTTP date. - Cap the attempts (three is a common default) and the total wait.
- Only when retries run out, return a tool result that tells the model the page is unavailable right now.
for attempt in range(3):
try:
return fetch(url)
except RateLimited as e:
sleep(e.retry_after or backoff(attempt))
except Timeout:
sleep(backoff(attempt))
return {"ok": False, "error": "TEMPORARILY_UNAVAILABLE",
"message": "The page did not respond after 3 tries. Use another source or try later."}
Hand permanent failures to the model
A permanent failure is a decision point, and the model is the right one to make it: try another source, search again, ask the user or say it could not check. It needs two things: a stable tag your code can log and test, and one plain sentence it can act on.
| Tag (example) | Sentence for the model |
|---|---|
CRAWL_NOT_FOUND | The page does not exist (404 or 410). Do not retry; find another source. |
CRAWL_NOT_RENDERED | The page needs JavaScript to show its content. Try another page or tell the user. |
UNSUPPORTED_CONTENT_TYPE | The URL is a file, not a web page. Use a tool for that file type. |
RESTRICTED_TARGET | This site cannot be fetched. Do not try other URLs on it. |
SOURCE_NOT_AVAILABLE | The site refused the request. Use another source. |
A refusal or a restricted target is the site's or the provider's decision: do not retry with URL variants or other tools to get around it. Sites' robots.txt and terms of service, and the provider's Acceptable Use Policy, still apply. This is not legal advice.
Anthropic's guidance on tools makes the same point about errors in general: an error response should steer the agent toward a better next call, not just report a code.
Partial failures in multi-URL calls
When one call reads several URLs, one failure must not fail the rest. Return one status per URL, and let the model see which pages it has and which it does not. Count successes and failures separately in your logs; a run where 4 of 10 pages failed is a different run from one where all 10 succeeded, even if the answer looks fine.
Stop conditions
Retries inside one call are capped; the loop around them needs caps too:
- a maximum number of fetches per task,
- a maximum number of failed fetches per task (for example 5) after which the agent must answer with what it has,
- no second attempt at the same URL with the same tag in one task.
Without these, an agent can walk a dead site one URL at a time until the step limit ends the run.
A worked example
Live Web Data API, made by the publisher of this site, uses this split. Every URL in a request gets a status with an error tag and message on failure, and failed, timed-out and restricted URLs cost 0 credits. The JavaScript and Python SDKs retry 429 (honoring Retry-After), 5xx and network errors up to three times with backoff and jitter, and never retry 400, 401, 402, 403 or 451. The MCP server returns errors as one actionable sentence ending in "No credits were charged." The full table of tags is in the API reference.
Where this breaks
- Errors that look like success. A consent page or a soft 404 returns 200 with the wrong text. A fetch tool that extracts the main content helps, but check titles in your evals.
- Retries that change meaning. Retrying a read is safe. Retrying a tool that sends, books or pays is not; keep those out of automatic retry entirely. See the prompt injection guide on separating read and act tools.
- Cost of a long failure path. Every failed attempt the model sees costs tokens even when the fetch is free. The token budget guide covers per-task limits.
Frequently asked questions
Should the LLM decide when to retry a failed tool call?
Not for transient failures. Retrying 429s, timeouts and 5xx responses is mechanical and belongs in code with backoff and a cap. The model should decide only what a retry cannot fix: whether to use another source, ask the user or give up.
How many times should an agent retry a web fetch?
A common starting point is up to three attempts with exponential backoff and jitter for transient errors, and zero for errors a retry cannot change, such as not found or a restricted target. Add a per-task limit so retries across many calls cannot add up.
Should a tool error be an exception or a tool result?
A tool result, for anything the model can react to. An exception usually ends the run or reaches the model as a stack trace. Reserve exceptions for problems the model cannot fix, such as an invalid API key or an empty balance.
What should a tool error message say?
One sentence on what happened and one on what to do next, in plain words. For example: the page needs JavaScript to show its content; try the site's documentation page or tell the user. Leave out stack traces and raw HTTP bodies.