The Definitive Guide to Structured Error Responses for AI Agents
Structured error responses are the single fastest way to improve your Agent Readiness Score. The canonical shape is six fields: error, code, message, request_id, retry_after, and details[]. This guide shows you the exact shape, five HTTP status codes with production-ready JSON, and copy-paste middleware for four frameworks.
Why Error Quality Determines Agent Success or Failure
When a human developer gets a 400 error, they read the message, check the docs, and fix their request. When an AI agent gets a 400 error, it needs machine-readable structure to decide what to do next. Is this a validation error it can fix by adjusting parameters? An auth error that requires a different token? A rate limit that requires waiting? Without structured fields, the agent is guessing.
Error quality impacts two dimensions of the Agent Readiness Score directly: D6 Data Quality (0.10 weight) and D9 Agent Experience (0.10 weight). Together that is 20% of your total score. Fixing error responses is the highest-ROI change most APIs can make — and it takes less than an hour.
The Canonical Error Shape
Every error response from your API should match this shape. Six fields, all typed, all predictable. Agents parse this shape once and handle every error your API can produce.
{
"error": true, // Always true on errors. Agents check this first.
"code": "MACHINE_READABLE_CODE", // Constant string. Never changes for same error type.
"message": "Human-readable text.", // Safe for logs. No internal details.
"request_id": "req_abc123", // Unique per request. Links to your server logs.
"retry_after": 30, // Seconds. Only on 429. Agents use this to wait.
"details": [ // Array of objects. Varies by error type.
{
"field": "email",
"constraint": "Must be valid email"
}
]
}errorAlways true. Lets agents distinguish error responses from successful ones without parsing status codes (which some HTTP libraries swallow).
codeMachine-readable constant like INVALID_PARAMETER, AUTH_REQUIRED, RATE_LIMITED. Agent switches on this value. Never changes for the same error type.
messageHuman-readable explanation safe for logging. Never contains stack traces, file paths, or internal system details.
request_idUnique per request (UUID or prefixed ID). Lets agents and support teams correlate errors with server-side logs.
retry_afterSeconds until the agent should retry. Required on 429 responses. Optional on 503 (maintenance). Agents use this instead of guessing.
detailsArray of objects with error-specific context. Validation errors list fields and constraints. Auth errors list required scopes. Agents iterate this programmatically.
Five HTTP Status Codes with Production-Ready JSON
These five status codes cover 95% of agent-facing errors. Each example shows the exact JSON your API should return. Copy these into your error handler and customize the messages.
{
"error": true,
"code": "INVALID_PARAMETER",
"message": "Field 'email' must be a valid email address.",
"request_id": "req_abc123def456",
"details": [
{
"field": "email",
"value": "not-an-email",
"constraint": "Must match pattern: *@*.*"
}
]
}{
"error": true,
"code": "AUTH_REQUIRED",
"message": "Bearer token is missing or expired.",
"request_id": "req_xyz789ghi012",
"details": [
{
"header": "Authorization",
"expected": "Bearer <token>",
"docs": "https://docs.example.com/auth"
}
]
}{
"error": true,
"code": "INSUFFICIENT_SCOPE",
"message": "Token lacks 'orders:write' scope.",
"request_id": "req_pqr345stu678",
"details": [
{
"required_scope": "orders:write",
"current_scopes": ["orders:read", "products:read"],
"upgrade_url": "https://dashboard.example.com/api-keys"
}
]
}{
"error": true,
"code": "NOT_FOUND",
"message": "Product with ID 'prod_999' does not exist.",
"request_id": "req_mno901vwx234",
"details": [
{
"resource_type": "product",
"resource_id": "prod_999",
"suggestion": "Use GET /products to list available products."
}
]
}{
"error": true,
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. 100 requests per minute allowed.",
"request_id": "req_jkl567yza890",
"retry_after": 32,
"details": [
{
"limit": 100,
"window": "60s",
"remaining": 0,
"reset_at": "2026-04-15T14:30:32Z"
}
]
}Six Anti-Patterns That Kill Agent Experience
We see these in 73% of the 500+ APIs we have scanned. Each one causes agents to retry incorrectly, waste tokens, or abandon the interaction entirely.
Framework-Specific Middleware
Copy-paste implementations for the four most common API frameworks. Each one produces the canonical error shape described above. Total implementation time: 10-15 minutes including testing.
// middleware/agentErrors.js
function agentErrorHandler(err, req, res, next) {
const requestId = req.headers['x-request-id'] || crypto.randomUUID()
const status = err.status || 500
res.status(status).json({
error: true,
code: err.code || 'INTERNAL_ERROR',
message: err.expose ? err.message : 'An internal error occurred.',
request_id: requestId,
...(err.retryAfter && { retry_after: err.retryAfter }),
...(err.details && { details: err.details }),
})
}
// app.use(agentErrorHandler) — after all routes// lib/apiError.ts
export function apiError(
status: number, code: string, message: string,
details?: Record<string, unknown>[]
) {
const requestId = crypto.randomUUID()
return Response.json(
{ error: true, code, message, request_id: requestId,
...(details && { details }) },
{ status, headers: { 'X-Request-ID': requestId } }
)
}
// Usage: return apiError(400, 'INVALID_PARAM', 'Email required')# middleware/agent_errors.py
import uuid
from rest_framework.views import exception_handler
def agent_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
request_id = str(uuid.uuid4())
response.data = {
"error": True,
"code": getattr(exc, "default_code", "ERROR"),
"message": response.data.get("detail", str(exc)),
"request_id": request_id,
}
response["X-Request-ID"] = request_id
return response# app/controllers/concerns/agent_errors.rb
module AgentErrors
extend ActiveSupport::Concern
included do
rescue_from StandardError do |e|
request_id = request.request_id
status = e.respond_to?(:status) ? e.status : 500
render json: {
error: true,
code: e.class.name.demodulize.underscore.upcase,
message: Rails.env.production? ? "Internal error" : e.message,
request_id: request_id
}, status: status
end
end
endThe 10-line version: If you can only do one thing, add an error handler that catches all exceptions, wraps them in the canonical shape, and returns JSON with the correct HTTP status code. The Express middleware above is literally 10 lines. That alone will improve your D6 and D9 scores measurably on the next AgentHermes scan.
How Error Quality Connects to the 9 Dimensions
Error handling is not an isolated concern — it touches half the scoring dimensions. Here is how structured errors ripple through your Agent Readiness Score:
D2 API Quality (0.15)
Correct HTTP status codes and JSON content type. Auth-protected endpoints that return 401+JSON score 87% of a 200.
D6 Data Quality (0.10)
Typed fields, consistent shape, machine-parseable details array. Agents can programmatically handle every error.
D7 Security (0.12)
No stack traces, no internal paths, no database errors in production responses. Safe message field.
D8 Reliability (0.13)
Proper 429 with retry_after headers. Status page links in 503 responses. Request IDs for incident correlation.
D9 Agent Experience (0.10)
The overall "can an agent use this API without human help?" dimension. Structured errors are the foundation.
Combined Impact
Error quality touches 0.60 of your total score weight. No other single change affects this many dimensions.
Frequently Asked Questions
Why do AI agents need structured errors more than human developers?
Human developers read error messages and make judgment calls. They see "invalid email" and know to fix the email field. AI agents parse JSON programmatically. They need a typed code field to branch logic, a details array to iterate specific failures, and a retry_after field to know when to try again. Without structure, agents either retry blindly or give up entirely — both waste resources.
What is the minimum viable error response for agent compatibility?
Three fields: error (boolean true), code (machine-readable string like INVALID_PARAMETER), and message (human-readable explanation). This takes 5 minutes to implement and immediately improves D6 Data Quality and D9 Agent Experience scores. Adding request_id and details is the next step, and retry_after is essential for any rate-limited endpoint.
How does AgentHermes detect error quality?
During a scan, AgentHermes sends intentionally malformed requests (missing auth, invalid parameters, non-existent resources) and analyzes the responses. It checks: Is the response JSON? Does it have a consistent shape? Is the HTTP status code correct (not 200-with-error-body)? Is there a request_id? Is retry_after present on 429s? Each check contributes to D6 Data Quality (0.10 weight) and D9 Agent Experience (0.10 weight).
Should error responses include stack traces?
Never in production. Stack traces leak internal architecture (file paths, library versions, database schemas) which is a security risk (D7 Security). In development/sandbox, include them in a separate debug field that is stripped in production. The message field should always contain a safe, actionable description.
See how your error responses score
AgentHermes scans your API error responses across all 9 dimensions. Run a free scan to see where your errors help — and where they hurt.