When an A2A call goes wrong, the caller sees one of three things: a transport failure such as a refused connection or a 401, a protocol error object saying the request could not be served, or a successful response carrying a Task that failed or was rejected. Code that mixes these up retries things that can never succeed and gives up on things that would have worked a second later.

This page is a reference for the A2A protocol error layer. It covers the nine A2A-specific errors and how each binding represents them, the reason strings that make them unambiguous, and code for both sides of the wire: a server mapper that emits all three bindings and a client classifier that decides what to do. It was checked against the A2A specification source on 2026-10-02, which lists 1.0.0 as the latest released version. For the wider picture of failures between agents, see the A2A error handling guide; for what to do after a crash or a dropped stream, see A2A error recovery.

Advertisement

Three kinds of failure, and why the distinction matters

Transport and authentication failures happen before A2A semantics apply. Examples are DNS failures, TLS errors, timeouts, and HTTP 401 or 403 from the authentication layer. The specification gives 401 Unauthorized and UNAUTHENTICATED as example codes for authentication problems, and 403 Forbidden and PERMISSION_DENIED for authorization. Neither has an A2A-specific code. Fix credentials or scopes. Do not retry them in a loop.

Protocol errors mean the agent understood that a request arrived but would not or could not serve it. These carry a code, a human-readable message and an optional array of typed details. There are two families: the standard JSON-RPC codes and the nine A2A-specific errors.

Task outcomes are not errors on the wire. If the agent accepts a message, works on it and fails, the call succeeds and returns a Task in TASK_STATE_FAILED. If the agent declines the work, the Task is in TASK_STATE_REJECTED. TASK_STATE_INPUT_REQUIRED and TASK_STATE_AUTH_REQUIRED are interrupted states that wait for the client. Treating a FAILED task as a transport error is a classic bug: resending the message creates a second task rather than repairing the first.

Classifying an A2A response before acting on itResponse arrivesany bindingHTTP 401 / 403?transport auth layeryesFix credentialsnot an A2A codenoError object present?error / status / detailsnoRead Task stateFAILED, REJECTED are resultsyesExtract reasonErrorInfo, else codeRetry with backoff-32603, INTERNAL, 503Fix the request-32602, -32005, -32008, -32009Reconcile state-32001, -32002, -32004Degrade capability-32003, -32007, -32004Only the left-hand bucket is safe to retry automatically, and only with the same messageId.-32004 appears twice: it can mean a missing capability or an operation on a terminal task.
Figure 1. Classify first, act second. Authentication sits outside A2A codes, failed tasks arrive as successful responses, and every protocol error falls into one of four action buckets.

The code table

Section 5.4 of the specification gives the canonical mapping. Every binding must represent all nine errors, and custom bindings must define an equivalent mapping that keeps the meaning. The reason column is the google.rpc.ErrorInfo reason. It is the error name in UPPER_SNAKE_CASE with the Error suffix removed, under the domain a2a-protocol.org.

ErrorJSON-RPCgRPCHTTPErrorInfo reasonMeaning
TaskNotFoundError-32001NOT_FOUND404TASK_NOT_FOUNDThe task ID is unknown, expired, purged, or not visible to this caller.
TaskNotCancelableError-32002FAILED_PRECONDITION400TASK_NOT_CANCELABLECancel was requested on a task already in a terminal state.
PushNotificationNotSupportedError-32003FAILED_PRECONDITION400PUSH_NOTIFICATION_NOT_SUPPORTEDPush config was used but the Agent Card says pushNotifications is not supported.
UnsupportedOperationError-32004FAILED_PRECONDITION400UNSUPPORTED_OPERATIONThe operation, or one aspect of it, is not supported, for example streaming when capabilities.streaming is false.
ContentTypeNotSupportedError-32005INVALID_ARGUMENT400CONTENT_TYPE_NOT_SUPPORTEDA media type in the message parts is not accepted by the agent or skill.
InvalidAgentResponseError-32006INTERNAL500INVALID_AGENT_RESPONSEAn agent returned a response that does not conform to the spec for the method.
ExtendedAgentCardNotConfiguredError-32007FAILED_PRECONDITION400EXTENDED_AGENT_CARD_NOT_CONFIGUREDThe extended Agent Card was requested but none is configured.
ExtensionSupportRequiredError-32008FAILED_PRECONDITION400EXTENSION_SUPPORT_REQUIREDThe card marks an extension required and the client did not declare it.
VersionNotSupportedError-32009FAILED_PRECONDITION400VERSION_NOT_SUPPORTEDThe requested A2A-Version is not served by this interface.

Look at the HTTP column. Seven of the nine errors return 400, and six of the gRPC statuses are FAILED_PRECONDITION. The status code alone cannot tell a cancel race from a version mismatch. That is why the spec requires an ErrorInfo object in the details for A2A-specific errors on the gRPC and HTTP bindings, and recommends one on JSON-RPC. Clients should branch on the reason string and treat the status code only as a fallback.

The standard JSON-RPC 2.0 codes still apply under the JSON-RPC binding: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params and -32603 internal error. JSON-RPC 2.0 reserves -32000 to -32099 for implementation-defined server errors, and A2A uses -32001 to -32099 for its own codes. Codes -32010 and up are unassigned today, and a later revision may assign them, so do not put your own errors there. Return a standard code and attach an ErrorInfo with your own domain instead.

Advertisement

One error, three wire shapes

Here is the same TaskNotFoundError in each binding. The JSON-RPC binding puts the details array in error.data:

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32001,
    "message": "Task not found",
    "data": [{
      "@type": "type.googleapis.com/google.rpc.ErrorInfo",
      "reason": "TASK_NOT_FOUND",
      "domain": "a2a-protocol.org",
      "metadata": {"taskId": "task-123"}
    }]
  }
}

gRPC returns a google.rpc.Status with code NOT_FOUND and the same ErrorInfo inside status.details. The HTTP+JSON binding returns HTTP 404 with a google.rpc.Status JSON body:

HTTP/1.1 404 Not Found
Content-Type: application/a2a+json

{"error": {"code": 404, "status": "NOT_FOUND",
  "message": "The specified task ID does not exist or is not accessible",
  "details": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
               "reason": "TASK_NOT_FOUND", "domain": "a2a-protocol.org",
               "metadata": {"taskId": "task-123"}}]}}

One inconsistency is worth knowing about. Section 11.6 defines HTTP errors as google.rpc.Status JSON. Some of the spec's own worked examples (the version-negotiation and task-listing scenarios) show application/problem+json bodies with fields such as type, title and detail, and one includes a supportedVersions list. Treat 11.6 as the rule when you build a server. When you build a client, accept both shapes, because some peers will copy the examples.

When to raise each error

TaskNotFoundError covers both "does not exist" and "not accessible to you". Use it for another tenant's task too. If you return 403 for a task that exists but is not yours, you have told the caller that the ID is real. TaskNotCancelableError is the normal loser of a race: a cancel arrives while the executor is committing a final state, and whichever transition commits first under the task lock wins. It is not an alarm. The client should read the task and accept its final state.

UnsupportedOperationError has two jobs. The spec requires it when a client calls SendStreamingMessage or SubscribeToTask against a card whose capabilities.streaming is false. The spec also requires it for subscribing to a task that is already terminal; in that case the client falls back to GetTask. PushNotificationNotSupportedError and ExtendedAgentCardNotConfiguredError are capability gaps that the client could have found in the Agent Card before calling.

ContentTypeNotSupportedError is about media types in message parts, such as sending a PDF to a text-only skill. Attach a google.rpc.BadRequest field violation that names the part. ExtensionSupportRequiredError fires when the card marks an extension as required and the request did not list it in the A2A-Extensions service parameter, which is a comma-separated list of extension URIs. VersionNotSupportedError is driven by A2A-Version, sent as a header or query parameter in Major.Minor form. An empty value is treated as 0.3 for backward compatibility, so a 1.0-only server will reject clients that send no header.

InvalidAgentResponseError is different from the others. It describes a response from an agent that breaks the spec, so it is usually raised by whoever received the bad response, such as a client SDK, a gateway or an orchestrator. If a peer returns a malformed Task, surface -32006 to your own caller instead of -32603, so they know the fault is downstream.

Server side: one exception type, three encoders

Keep the error model in one place and encode it at the edge. The sketch below is framework-neutral Python. Handlers raise A2AError, and each binding adapter turns it into its own wire form.

from dataclasses import dataclass, field

DOMAIN = "a2a-protocol.org"
# name -> (json-rpc code, grpc status, http status)
TABLE = {
    "TASK_NOT_FOUND":                     (-32001, "NOT_FOUND", 404),
    "TASK_NOT_CANCELABLE":                (-32002, "FAILED_PRECONDITION", 400),
    "PUSH_NOTIFICATION_NOT_SUPPORTED":    (-32003, "FAILED_PRECONDITION", 400),
    "UNSUPPORTED_OPERATION":              (-32004, "FAILED_PRECONDITION", 400),
    "CONTENT_TYPE_NOT_SUPPORTED":         (-32005, "INVALID_ARGUMENT", 400),
    "INVALID_AGENT_RESPONSE":             (-32006, "INTERNAL", 500),
    "EXTENDED_AGENT_CARD_NOT_CONFIGURED": (-32007, "FAILED_PRECONDITION", 400),
    "EXTENSION_SUPPORT_REQUIRED":         (-32008, "FAILED_PRECONDITION", 400),
    "VERSION_NOT_SUPPORTED":              (-32009, "FAILED_PRECONDITION", 400),
}

@dataclass
class A2AError(Exception):
    reason: str                      # key of TABLE
    message: str                     # safe for the caller to read
    metadata: dict = field(default_factory=dict)
    extra: list = field(default_factory=list)   # e.g. a BadRequest detail

    def details(self):
        info = {"@type": "type.googleapis.com/google.rpc.ErrorInfo",
                "reason": self.reason, "domain": DOMAIN,
                "metadata": {k: str(v) for k, v in self.metadata.items()}}
        return [info, *self.extra]

def to_jsonrpc(err, req_id):
    code, _, _ = TABLE[err.reason]
    return {"jsonrpc": "2.0", "id": req_id,
            "error": {"code": code, "message": err.message, "data": err.details()}}

def to_http(err):
    _, grpc_status, http = TABLE[err.reason]
    body = {"error": {"code": http, "status": grpc_status,
                      "message": err.message, "details": err.details()}}
    return http, {"Content-Type": "application/a2a+json"}, body

def cancel_task(store, task_id, principal):
    task = store.get(task_id, principal)          # None if absent OR not visible
    if task is None:
        raise A2AError("TASK_NOT_FOUND", "Task not found", {"taskId": task_id})
    if task.state in {"TASK_STATE_COMPLETED", "TASK_STATE_FAILED",
                      "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"}:
        raise A2AError("TASK_NOT_CANCELABLE",
                       f"Task is already {task.state}", {"taskId": task_id})
    return store.request_cancel(task)

Unexpected exceptions should become -32603 or HTTP 500 with a generic message and a correlation ID in the metadata, never a stack trace. Metadata values are strings, following ErrorInfo's convention.

Client side: normalise, then decide

The client's job is to reduce any of the wire shapes to one decision. This classifier accepts JSON-RPC errors, google.rpc.Status JSON and problem+json bodies, prefers the ErrorInfo reason, and falls back to the numeric code and then the HTTP status.

CODE_TO_REASON = {code: name for name, (code, _, _) in TABLE.items()}

ACTION = {
    "TASK_NOT_FOUND": "reconcile",       # forget local handle; re-create only if safe
    "TASK_NOT_CANCELABLE": "reconcile",  # GetTask and accept the terminal state
    "UNSUPPORTED_OPERATION": "degrade",  # e.g. fall back from streaming to polling
    "PUSH_NOTIFICATION_NOT_SUPPORTED": "degrade",
    "EXTENDED_AGENT_CARD_NOT_CONFIGURED": "degrade",
    "CONTENT_TYPE_NOT_SUPPORTED": "fix_request",
    "EXTENSION_SUPPORT_REQUIRED": "fix_request",   # add the URI to A2A-Extensions
    "VERSION_NOT_SUPPORTED": "fix_request",        # re-read the card, pick a version
    "INVALID_AGENT_RESPONSE": "report",            # downstream agent is broken
}

def classify(http_status, body):
    if http_status in (401, 403):
        return "auth", None
    err = body.get("error") if isinstance(body, dict) else None
    if err is None and isinstance(body, dict) and "status" in body and "title" in body:
        err = {"code": body["status"], "details": []}          # problem+json
    if err is None:
        return "ok", None                                      # inspect Task state next
    details = err.get("data") or err.get("details") or []
    reason = next((d.get("reason") for d in details
                   if d.get("@type", "").endswith("google.rpc.ErrorInfo")
                   and d.get("domain") == DOMAIN), None)
    reason = reason or CODE_TO_REASON.get(err.get("code"))
    if reason:
        return ACTION.get(reason, "report"), reason
    if err.get("code") in (-32603,) or http_status in (500, 502, 503, 504):
        return "retry", "INTERNAL"
    return "fix_request", err.get("code")                      # -32700/-32600/-32601/-32602

"Retry" here means resend with the same messageId so the server can deduplicate it. That is covered in the A2A idempotency guide. Use capped exponential backoff with jitter, and give up after a fixed budget.

Worked example: a cancel that loses the race

An orchestrator delegates a translation task. The user aborts, and the orchestrator sends CancelTask at the same moment the remote executor writes TASK_STATE_COMPLETED. The executor commits first. The cancel handler then finds a terminal task and raises TASK_NOT_CANCELABLE, so the client receives -32002 with reason TASK_NOT_CANCELABLE. The classifier returns "reconcile". The orchestrator calls GetTask, sees COMPLETED with an artifact, and has to decide whether to discard the result or keep it. The protocol has done its job. What remains is a product decision, and the code should make it visible instead of logging an error.

Now suppose a proxy strips the details array and returns a bare HTTP 400. With no reason and no code, the classifier treats it as a bad request. Proxies must pass error bodies through unchanged, so test error paths through every hop.

Operating it: metrics, alerts and failure modes

  • Count by reason, not by status. Emit a counter labelled with method, ErrorInfo reason and peer agent. A rise in VERSION_NOT_SUPPORTED after a deploy means a client population was left behind. A rise in TASK_NOT_FOUND often means task retention is shorter than clients expect.
  • Alert on INVALID_AGENT_RESPONSE. It means a contract break between agents, and it usually follows a peer upgrade.
  • Do not page on TASK_NOT_CANCELABLE. It is expected under concurrency.
  • Messages are for humans and details are for machines. Never parse message, and never put secrets or stack traces in it.
  • Keep FAILED tasks out of protocol error metrics. Track task failure rate separately, by skill. Mixing the two hides a broken agent behind a healthy protocol layer, or the other way round.

Trade-offs

ChoiceBenefitCost
Branch on ErrorInfo reasonUnambiguous across bindingsBreaks if a proxy strips details; keep a code fallback
Return NOT_FOUND for others' tasksNo existence leak between tenantsHarder support debugging; log the real cause server-side
Fail a task (FAILED) vs raise an errorFAILED keeps history and artifacts on the taskClients must inspect results, not only error paths

For the request and response mechanics that these errors travel over, read the JSON-RPC request/response flow walkthrough.

What to do next

  1. Grep your server for every error it can return and check each against the table above: code, gRPC status, HTTP status and reason.
  2. Make sure every A2A-specific error carries an ErrorInfo with domain a2a-protocol.org, including on JSON-RPC, where it is recommended rather than required.
  3. Replace any client branching on HTTP status alone with a reason-first classifier like the one above, and make it accept problem+json.
  4. Separate task failure metrics from protocol error metrics, and label both by peer agent.
  5. Add one end-to-end test per error that passes through every proxy and gateway in production.
  6. Decide what happens when a cancel loses the race, and write that behaviour into the orchestrator.
Key takeaway: A2A has three failure layers: transport and authentication, protocol errors, and task outcomes. Each one needs a different response. The nine A2A-specific errors take JSON-RPC codes -32001 to -32009 and share a small set of gRPC and HTTP statuses, so the google.rpc.ErrorInfo reason, under domain a2a-protocol.org, is what tells them apart. Servers should raise one error type and encode it per binding. Clients should normalise every shape, including the problem+json bodies in some spec examples, retry only transient internal failures with the same messageId, and treat FAILED and REJECTED tasks as results rather than errors.