Your Tool Returns a 500 — Then What? Designing a Tool Error Contract
▶ Watch on YouTube & subscribe to The Stack Underflow
A tool you wired up last sprint calls a third-party API. The API is fine 99% of the time, and then one afternoon it starts returning HTTP 500. Somewhere in your code there’s a decision — made by whoever wrote that tool — about what happens next. Does the whole agent run die with a stack trace? Does the agent quietly tell the user “done!” while nothing happened? Or does the agent notice the failure, reason about it, and try something else? That decision is not a bug-fix detail. It’s a design choice that determines whether your agent is resilient or brittle, and it’s exactly the kind of question Domain 2 of the Claude Certified Architect exam tests.
The one-sentence version: A tool should never raise an exception into the agent loop — it should catch the failure and return a structured error object, because the error you return is a second prompt the model reads and reasons over.
The question
A tool’s upstream API returns HTTP 500. What should the tool return to the agent?
- A) Throw and crash the run
- B) An empty string
- C) A structured error, never an exception: set
is_erroron the tool_result and return{isError, errorCategory, isRetryable} - D) Retry silently forever
Pause here and pick an answer before you keep reading.
The answer
The correct option is C. When the upstream API fails, the tool should never let an exception propagate out of it and crash the run. Instead, it catches the failure and hands back a structured, machine-readable error — flagging the block itself as an error, and putting enough detail in the payload for the agent to make a real decision about what to do next.
Here’s the mechanism, matching the beat-sheet from the practice set this question comes from:
UPSTREAM API TOOL (your code) AGENT / MODEL
───────────── ───────────────── ──────────────
GET /orders/9182
│
▼
HTTP 500 Internal
Server Error
│
└──────────────► catch(err) {
// never rethrow into the loop
return tool_result {
is_error: true, ← protocol flag
content: {
"isError": true,
"errorCategory":
"upstream_unavailable",
"isRetryable": true,
"detail": "orders API
returned 500"
}
}
}
│
▼
tool_result block enters
the conversation as CONTEXT
│
▼
Model reads is_error + the
JSON payload and decides:
• retry the call
• fall back to a cached value
• tell the user it's degraded
• escalate to a human
Two casing conventions are doing real work in that diagram, and it’s worth noticing them rather than glossing over them. is_error (snake_case) is the protocol-level flag on the tool_result content block — it’s how the transport layer marks “this call did not succeed” independent of whatever is inside. isError, errorCategory, and isRetryable (camelCase) live inside the content payload — they’re just JSON you designed, and the model reads them the same way it reads any other tool output. The protocol flag tells the harness “don’t treat this as a normal result.” The payload tells the model what kind of failure it was and what its options are.
That second part is the whole point. A raw HTTP 500 is meaningless to a language model on its own — it’s just a number. But errorCategory: "upstream_unavailable" combined with isRetryable: true is information the model can reason over exactly the way it reasons over a paragraph of instructions. That’s why the framing “the error you return is a second prompt” matters: everything a tool returns, success or failure, becomes context the model conditions its next action on. A well-designed error is a small brief for the model’s next decision. A stack trace or an empty string is not.
Why the other options fail
- A) Throw and crash the run. An unhandled exception doesn’t stay inside the tool — it propagates up and kills the agent loop entirely. The user gets nothing: no answer, no partial result, no explanation. You’ve converted a recoverable upstream hiccup into a total outage of your own agent, which is strictly worse than the failure that triggered it.
- B) An empty string. This is the quietly dangerous option. An empty string isn’t flagged as an error, so the model has no signal that anything went wrong. It’s very likely to reason “the tool ran and found nothing” and confidently report an incorrect result to the user — a hallucination seeded by your own tool, not the model’s imagination.
- D) Retry silently forever. Retrying can be a fine part of a recovery strategy, but only when it’s bounded, visible, and a decision the agent (or a deliberate retry policy) makes on purpose. An unbounded silent retry loop can hang the run indefinitely, burn API quota against a service that’s already down, and gives the model zero visibility into the fact that anything is wrong. Retry logic belongs behind the
isRetryablesignal, not baked in as an automatic, invisible loop inside the tool.
The concept behind it
Generalize this past one HTTP 500 and you get a reusable principle for any tool you design: failure is not an exceptional case to be swallowed by infrastructure — it’s a first-class output your tool must design for.
A structured tool-error contract typically carries three kinds of information:
| Field | Purpose | Example values |
|---|---|---|
isError | A boolean the model can branch on immediately | true / false |
errorCategory | What kind of failure this was, so the model picks the right recovery | upstream_unavailable, invalid_input, not_found, permission_denied, rate_limited |
isRetryable | Whether trying again is even worth it | true for a transient 500 or timeout, false for a 404 or a bad request |
Once you have categories like this, the model can actually differentiate its response. A rate_limited error with isRetryable: true might justify a short backoff and one retry. A permission_denied error should never be retried — it should be surfaced to the user or escalated, because retrying won’t change the outcome. Collapsing all of these into “it failed” (or worse, a bare exception) removes the information the model needs to tell them apart.
This is also why the pattern lives specifically at the tool layer, not the model layer. The tool is the only thing that knows the real shape of the failure — it saw the actual HTTP status, the actual timeout, the actual malformed response. Its job is to translate that low-level detail into a small, model-legible vocabulary the agent can act on, the same way a well-designed tool description translates an API’s capabilities into language the model can act on before the call. Error handling and tool description are the same discipline applied at two different moments: one shapes what the model expects going in, the other shapes what it learns coming out.
It also matters for anything downstream of the immediate call. If your agent has a broader recovery strategy — retry with backoff, fall back to a cached value, hand off to a human — none of that logic can run correctly if the tool’s failure signal is a stack trace the harness swallowed or an empty string that looks like success. Structured errors are the interface contract that makes higher-level error propagation and escalation logic possible at all; if you want the fuller picture of how that plays out across a whole agent (not just one tool call), that’s covered in error propagation and escalation patterns elsewhere in this series.
FAQ
Should a tool ever throw an exception? Reserve exceptions for truly unrecoverable situations at the framework or infrastructure level — the kind of failure no amount of agent reasoning can work around, like the process running out of memory. For anything the model has a plausible path to react to — a downstream API being down, a malformed input, a rate limit — return a structured error instead of throwing. If in doubt, ask: “could a reasonable agent do something useful with this information?” If yes, it belongs in the return value, not an exception.
What’s the difference between is_error on the tool_result and the fields inside the payload?
is_error is a protocol-level signal recognized by the harness/transport — it marks the block as a failed call, independent of what’s inside it. The fields inside the content (isError, errorCategory, isRetryable, or whatever schema you design) are ordinary JSON that only the model interprets. You generally want both: the flag so infrastructure and logging can treat it correctly, and the payload so the model has enough detail to reason about recovery.
Does this mean the tool should decide whether to retry?
No — the tool reports whether retrying is plausible (isRetryable), but the decision to actually retry, how many times, and with what backoff belongs to the agent or an explicit retry policy wrapping the call. Baking an automatic retry loop into the tool itself removes visibility and can turn a transient failure into a long silent hang, which is exactly what option D gets wrong.
How does this interact with MCP specifically?
The Model Context Protocol carries the same tool_result / is_error shape end to end — an MCP server’s tool implementation is exactly the layer that should catch upstream failures and return a structured error rather than letting a raw exception cross the MCP boundary. If you’re still getting oriented on how MCP wires a tool call end to end, MCP Explained on One Diagram is the place to start.
Not affiliated with, authorized, or endorsed by Anthropic. “Claude” and “Claude Certified Architect” are trademarks of Anthropic. These are original practice questions for study, not real exam content — verify current exam details on Anthropic’s official pages.
Where this fits in the CCA series
This is Question 3 of the Domain 2 (Tool Design & MCP Integration, ~18% of the exam) practice set — as of mid-2026 the CCA-F runs on Pearson VUE, costs $125, allows retakes, and the exam itself is 60 questions in 120 minutes with a passing scaled score of 720/1000 across five domains; verify these logistics on Anthropic’s official pages before you register. For the full map of all five domains, start with the Claude Certified Architect (CCA) Exam Guide.
Staying inside Domain 2, this question pairs naturally with Why Does Claude Pick the Wrong Tool? and The Model Sends ‘Next Tuesday,’ Not a Date — together the three cover the description, the schema, and the error contract, which are the three places a tool call can go wrong before it ever reaches the agent’s reasoning. Once a tool is returning clean results instead of raw dumps, Your Tool Returns 5,000 Rows covers the matching problem on the success path. Browse all tutorials for the rest of the series.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →