Error Handling
RFC 9457 Problem Details
The current standard for structured HTTP error responses that agents can parse and act on
Last verified 2026-09-24
Summary
RFC 9457 standardizes error response format with five core fields: type (stable URI), title, status, detail, instance. Agents branch on type, never on title or detail. Extensions like is_retriable, retry_after_ms, suggestions, trace_id, doc_uri enable autonomous recovery.
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate limit exceeded",
"status": 429,
"is_retriable": true,
"retry_after_ms": 5000,
"suggestions": ["Wait 5 seconds and retry"]
}- type: stable URI agents can branch on (contract-level)
- is_retriable: explicit boolean, not inferred from status code
- suggestions: concrete next steps, first is most likely fix
- trace_id: correlation ID for internal lookup
- Content-Type: application/problem+json signals RFC 9457 format
RFC 9457 (July 2023) defines a standard JSON format for HTTP error responses. It obsoletes RFC 7807 (2016). Like its predecessor, it answers a fundamental problem: HTTP status codes alone are insufficient for agents to decide whether to retry, modify the request, or escalate.
The format has become widespread. The application/problem+json content type signals that the response body follows the standard, allowing agents and client libraries to handle errors consistently regardless of which service returned them.
The Standard Fields
{
"type": "https://example.com/errors/insufficient-credit",
"title": "Insufficient credit",
"status": 422,
"detail": "The account has $12.00 remaining but the transaction costs $42.00.",
"instance": "/accounts/acme/transactions/txn_01HV3K8MNP"
}| Field | Required | Description |
|---|---|---|
type | Recommended | A URI that uniquely identifies the error type. Should dereference to human-readable documentation. |
title | Recommended | A short, human-readable summary of the problem type. Does not change between occurrences. |
status | Optional | The HTTP status code. Mirrors the response's actual status code. |
detail | Optional | A human-readable explanation specific to this occurrence of the problem. |
instance | Optional | A URI reference that identifies the specific occurrence of the problem. |
The type URI is the stable, parseable identifier that agents can branch on:
const response = await fetch("/accounts/acme/charge", {
method: "POST",
body: JSON.stringify({ amount: 4200 }),
});
if (!response.ok) {
const problem = await response.json();
switch (problem.type) {
case "https://example.com/errors/insufficient-credit":
// Specific recovery: check balance, reduce amount, or escalate
break;
case "https://example.com/errors/account-suspended":
// Not recoverable: stop and report to human
break;
case "https://example.com/errors/validation-error":
// Fix the request and retry
break;
default:
// Unknown error type: log and escalate
}
}Agents should never branch on the title or detail fields - those are prose for humans and may change between releases. type is the stable contract.
RFC 9457 vs RFC 7807
RFC 9457 retains full backward compatibility with RFC 7807 while adding:
- Formal type registration: The IANA HTTP Problem Types registry gives
typeURIs a place to be registered and documented. It covers thetypevalues themselves, not extension member names likeis_retriableortrace_id- there is no registry for those; document them in your own reference. - Internationalization support: Extensions may include language tags and localized explanations.
- Security clarifications: Explicit guidance on preventing information disclosure (no stack , internal error codes sanitized).
- Instance URI semantics: Clearer definition of when
instanceshould be a server-relative URI vs absolute URI.
For practical purposes, both standards share the core five fields and the extension mechanism. Use RFC 9457 as the baseline for new APIs.
Recommended Extensions for Agent APIs
The RFC explicitly allows extension fields beyond the five core ones. For agent-readable APIs, these extensions carry the structured recovery information agents need to decide whether to retry unchanged, modify and retry, or escalate. They are not part of the RFC itself and have no IANA registry of their own - document them in your own API reference instead.
| Field | Type | Purpose |
|---|---|---|
is_retriable | boolean | Retry the same request unchanged? |
retry_after_ms | number | Milliseconds to wait before retrying |
doc_uri | string | Link to detailed error documentation |
trace_id | string | Correlation ID for internal lookup |
suggestions | string[] | Ordered recovery steps (first is the most likely) |
errors | Problem[] | Nested Problem Details for batch / multi-field cases |
is_retriable
A boolean that explicitly tells the agent whether retrying the same request unchanged might succeed. false marks a terminal error where the request itself is wrong - do not infer retriability from the status code alone:
{
"type": "https://example.com/errors/service-unavailable",
"title": "Service temporarily unavailable",
"status": 503,
"detail": "The invoices service is undergoing maintenance.",
"is_retriable": true,
"retry_after_ms": 30000
}retry_after_ms
How long to wait before retrying, in milliseconds. It is the numeric equivalent of the Retry-After header, carried in the body for clients that process the body but not headers - and preferred over the header for parsing consistency. Agents can sleep on it directly:
if (error.is_retriable && error.retry_after_ms) {
await sleep(error.retry_after_ms);
return retry();
}{
"type": "https://example.com/errors/rate-limit-exceeded",
"title": "Rate limit exceeded",
"status": 429,
"detail": "You have exceeded the 100 requests per minute limit.",
"is_retriable": true,
"retry_after_ms": 47000
}suggestions
An array of actionable next steps, ordered so the first is the most likely fix. Short, imperative strings describing what the agent should try:
{
"type": "https://example.com/errors/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "The request body failed schema validation.",
"suggestions": [
"Provide a value for the required 'amount' field",
"The 'currency' field must be a 3-letter ISO 4217 code (e.g., 'USD')"
]
}doc_uri
A link to detailed documentation for this error type. Unlike type, which is a stable machine identifier, doc_uri may point to versioned docs or a troubleshooting guide a human can follow up on:
{
"type": "https://example.com/errors/webhook-validation-failed",
"title": "Webhook signature validation failed",
"status": 401,
"detail": "The X-Webhook-Signature header does not match the expected HMAC-SHA256 signature.",
"doc_uri": "https://docs.example.com/webhooks/security#signature-validation"
}trace_id
A correlation ID linking this error to a specific request in the service's observability stack - server logs and OpenTelemetry spans. When an agent escalates to a human, the human or support team can look up the trace without reproducing the issue:
{
"type": "https://example.com/errors/internal-error",
"title": "Internal server error",
"status": 500,
"detail": "An unexpected error occurred. The issue has been logged.",
"trace_id": "01HV3K8MNP2QRS3TUVWX",
"is_retriable": true,
"retry_after_ms": 5000
}errors
RFC 9457 allows aggregating multiple problems under an errors array - useful for batch operations or multi-field validation. Each element follows the same Problem Details schema, so an agent can recurse into it to recover per item:
{
"type": "https://example.com/errors/batch-failure",
"title": "Batch operation partially failed",
"status": 422,
"detail": "3 of 5 items failed validation",
"errors": [
{
"type": "https://example.com/errors/validation-error",
"detail": "Item 0: 'amount' is required",
"is_retriable": true
},
{
"type": "https://example.com/errors/validation-error",
"detail": "Item 2: 'currency' is invalid",
"is_retriable": true
}
]
}Validating Problem Details
A single schema keeps error responses consistent across API handlers and output. This Zod schema covers the core fields plus the agent extensions:
import { z } from "zod";
const ProblemDetails = z.object({
type: z.string().url().optional().default("about:blank"),
title: z.string(),
status: z.number().int().min(400).max(599),
detail: z.string(),
instance: z.string().optional(),
is_retriable: z.boolean().optional(),
retry_after_ms: z.number().int().optional(),
doc_uri: z.string().url().optional(),
trace_id: z.string().optional(),
suggestions: z.array(z.string()).optional(),
errors: z.array(z.lazy(() => ProblemDetails)).optional(),
});
type ErrorResponse = z.infer<typeof ProblemDetails>;See templates/errors-and-auth/problem-details.ts for a production-ready implementation.
Handling Problem Details in an Agent
Agents branch twice: first on type to identify which error occurred (shown above), then on is_retriable to decide what to do about it. The recovery branch reads the extension fields:
async function callTool(tool: string, params: Record<string, unknown>): Promise<unknown> {
const response = await fetch(`/api/tools/${tool}`, {
method: "POST",
body: JSON.stringify(params),
});
if (!response.ok) {
const error = await response.json();
if (error.is_retriable === false) {
// Terminal error - escalate or fail the task
throw new Error(`${error.title}: ${error.detail}. See ${error.doc_uri}`);
}
if (error.is_retriable && error.retry_after_ms) {
// Transient error with explicit wait time
await sleep(error.retry_after_ms);
return callTool(tool, params); // Retry unchanged
}
if (error.suggestions && error.suggestions.length > 0) {
// Recoverable with modification - agent rewrites params
// (implementation depends on agent framework)
}
}
return response.json();
}Batch Error Aggregation
When processing multiple items, return partial success with per-item errors under the errors array rather than failing the whole batch:
async function batchCreateInvoices(invoices: InvoiceData[]): Promise<Response> {
const results = [];
const errors = [];
for (let i = 0; i < invoices.length; i++) {
try {
results.push(await createInvoice(invoices[i]));
} catch (err) {
errors.push({
type: "https://example.com/errors/validation-error",
title: "Validation failed",
status: 422,
detail: `Item ${i}: ${err.message}`,
instance: `batch-item-${i}`,
is_retriable: true,
});
}
}
if (errors.length > 0) {
return Response.json(
{
type: "https://example.com/errors/batch-failure",
title: "Batch operation partially failed",
status: 207, // Multi-status
detail: `${results.length} succeeded, ${errors.length} failed`,
errors,
},
{ status: 207 },
);
}
return Response.json(results);
}Cancellation and Intent Tracing
When an agent cancels an in-flight request, the error can carry an intent object describing the operation's state so the agent can reason about cleanup instead of blindly retrying:
{
"type": "https://example.com/errors/operation-cancelled",
"title": "Operation Cancelled",
"status": 408,
"detail": "Request aborted by client after 5000ms",
"trace_id": "span-xyz789",
"is_retriable": true,
"intent": {
"operation": "create_invoice",
"resource_id": "inv-pending-123",
"state_before_cancel": "payment_processing",
"recovery": "Operation may be in progress server-side. Poll GET /invoices/inv-pending-123 to check status."
}
}Content Type
Always include the Content-Type: application/problem+json header on error responses. This signals to clients and agents that the body is a parseable problem document:
function problemResponse(res: Response, status: number, problem: ProblemDetails): void {
res.status(status).setHeader("Content-Type", "application/problem+json").json(problem);
}Checklist
- All error responses use
application/problem+jsoncontent type - Every error has a stable
typeURI that agents can branch on -
is_retriableis present on every error response -
titleis stable;detailis occurrence-specific - Stack traces and internal details never appear in responses
Related Pages
- Designing Errors for Agent Recovery - structuring error content to enable recovery
- Retry Patterns - how agents should act on error information
- Templates: Problem Details Schema - Zod implementation