Nine A2A errors map into three bindings, and because their status codes collide, the
ErrorInforeason in the details is what tells them apart.
The planner asks test-runner for a task id that does not exist, once over each JSON binding. JSON-RPC answers HTTP/1.1 200 OK with code -32001, and HTTP+JSON answers HTTP/1.1 404 Not Found. Both carry the same ErrorInfo, with the reason TASK_NOT_FOUND, as figure shows.
When you finish this section, you can read an error in any binding, name the A2A error behind it, and choose what to do next.
Original source
id: fig-task-not-found
kind: comparison
title: TaskNotFoundError in two JSON bindings
claim: The same TaskNotFoundError arrives as HTTP 200 with code -32001 in JSON-RPC and as HTTP 404 in HTTP+JSON, carrying the same ErrorInfo.
caption: Left, GetTask over JSON-RPC from the error capture. Right, the same lookup over HTTP+JSON from the HTTP+JSON capture. The box at the bottom is the detail both replies carry. Ids shortened to 8 characters.The detail that names the error
The specification defines nine A2A errors and maps each one into every binding {{spec §3.3.2}} {{spec §5.4}}. JSON-RPC numbers them -32001 to -32009 {{spec §9.5}}, HTTP answers 400 for seven, and gRPC answers FAILED_PRECONDITION for six, as the error table lists.
Every error carries a code, a message, and optional details, each detail naming its type in @type {{spec §3.3.2}}. For A2A errors the deciding detail is a google.rpc.ErrorInfo:
Original source
label: the detail that names the error
source: spec §11.6
---
For an A2A error, an implementation must put a `google.rpc.ErrorInfo` object in the `details` array.Its reason is the error's name in upper snake case without the Error suffix, such as TASK_NOT_FOUND, and its domain is a2a-protocol.org {{spec §11.6}}. The gRPC binding carries the same MUST {{spec §10.6}}, and JSON-RPC only a SHOULD {{spec §9.5}}. The metadata is free-form: the kit fills it, and the reference SDK leaves it empty, as the kit README lists.
JSON-RPC errors arrive with HTTP 200
A JSON-RPC error is an ordinary response with HTTP 200. Its error member holds code, message, and data where result would be {{spec §9.5}}. The five standard JSON-RPC codes, -32700 and -32600 to -32603, keep their meaning {{spec §9.5}}.
Validation errors carry a different detail type, a google.rpc.BadRequest that names the field:
"message": {
"role": "ROLE_USER",
…
HTTP/1.1 200 OK
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32602,
"message": "Invalid parameters",
"data": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "message.messageId",
"description": "messageId is required"Original source
title: a message without messageId, refused with a BadRequest detail
source: capture/out/11-errors.http
lang: http
note: The request is cut to the start of its message, which has no messageId.
---
"message": {
"role": "ROLE_USER",
…
HTTP/1.1 200 OK
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32602,
"message": "Invalid parameters",
"data": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "message.messageId",
"description": "messageId is required"Do not match on message texts: the reference SDK answers the same case with "Validation failed" and names the field message.message_id {{sdk src/a2a/utils/proto_utils.py}}.
Authentication can fail before JSON-RPC runs, with HTTP 401 as the specification's example {{spec §3.3.2}}, so read the status first, as Security schemes shows.
HTTP+JSON errors are google.rpc.Status
An HTTP+JSON error uses the HTTP status and a JSON google.rpc.Status {{spec §11.6}}. Its error object repeats the status in code, names the gRPC status in status, and adds message and details:
POST /a2a/rest/tasks/bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3:cancel HTTP/1.1
…
HTTP/1.1 400 Bad Request
Content-Type: application/a2a+json
{
"error": {
"code": 400,
"status": "FAILED_PRECONDITION",
"message": "Task cannot be canceled",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "TASK_NOT_CANCELABLE",
"domain": "a2a-protocol.org",
"metadata": {
"taskId": "bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3",
"state": "TASK_STATE_COMPLETED"Original source
title: a cancel of a completed task over HTTP+JSON
source: capture/out/16-rest-binding.http
lang: http
note: The request headers and body are cut. Over JSON-RPC this error is code -32002, as in 10-cancel.http.
---
POST /a2a/rest/tasks/bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3:cancel HTTP/1.1
…
HTTP/1.1 400 Bad Request
Content-Type: application/a2a+json
{
"error": {
"code": 400,
"status": "FAILED_PRECONDITION",
"message": "Task cannot be canceled",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "TASK_NOT_CANCELABLE",
"domain": "a2a-protocol.org",
"metadata": {
"taskId": "bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3",
"state": "TASK_STATE_COMPLETED"Two samples in the specification still show application/problem+json bodies in the older RFC 9457 style {{spec §6.4}} {{spec §6.5}} (conflict D13). PR #1641 and PR #1689 replace those samples, and both are still open. The migration guide gives the type as application/json {{docs whats-new-v1}}, where §11.6 shows application/a2a+json. The SDKs send all three, and all answer JSON-RPC errors with HTTP 200:
| SDK | JSON-RPC errors | HTTP+JSON errors |
|---|---|---|
| a2a-python 1.2.2 | application/json {{sdk src/a2a/server/routes/jsonrpc_dispatcher.py}} |
application/json {{sdk src/a2a/utils/error_handlers.py}} |
| a2a-go | application/json {{sdk-go a2asrv/jsonrpc.go at v2.6.0}} |
application/json {{sdk-go a2asrv/rest.go at v2.6.0}} |
| a2a-java | application/json {{sdk-java reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java at v1.4.0.Final}} |
application/json {{sdk-java transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java at v1.4.0.Final}} |
| a2a-js | application/json {{sdk-js src/server/express/json_rpc_handler.ts at v1.3.0}} |
application/a2a+json {{sdk-js src/server/express/rest_handler.ts at v1.3.0}} |
| a2a-dotnet | application/json {{sdk-dotnet src/A2A.AspNetCore/JsonRpcResponseResult.cs at v1.0.0-preview2}} |
application/problem+json, a problem-details body {{sdk-dotnet src/A2A.AspNetCore/A2AHttpProcessor.cs at v1.0.0-preview2}} |
| a2a-rs | application/json {{sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1}} |
application/problem+json {{sdk-rust a2a-server/src/rest.rs at a2a-server-lf-v0.5.1}} |
Send application/a2a+json, and accept all three types when you read. Parse the body as google.rpc.Status whatever the type says. The exception is a2a-dotnet at this release, whose problem-details body carries no ErrorInfo. The gRPC binding puts the same three parts in status.code, status.message, and status.details {{spec §10.6}}.
What to retry
UnsupportedOperationError covers three problems with one code, -32004, and one reason, UNSUPPORTED_OPERATION {{spec §3.3.4}} {{spec §3.1.1}} {{spec §3.1.6}}. The card lacks streaming or the extended card, or the task is terminal, as in 10-cancel.http. Only message and metadata tell the cases apart, and the specification fixes neither. So when a task operation gets -32004, read the task with GetTask before you decide.
Most A2A errors describe the request or the task, so resending the same bytes gets the same answer:
-32603, HTTP500or503, or a dropped connection: retry with backoff, and honorRetry-After{{spec §3.3.2}}.- HTTP
401: get a fresh credential, then retry. -32600,-32602,-32005,-32009, and their400forms: fix the request before you send it again.-32001: stop, because the id is wrong, purged, or hidden from you {{spec §3.3.2}}.-32002, or-32004on a terminal task: start a new task, as CancelTask and terminal states shows.-32003,-32007, or-32004from a capability check: read the card again.
A retried SendMessage can start a second task, because duplicate detection by messageId is optional {{spec §3.3.1}}. Send the same messageId anyway, as Message explains.
Original source
- Read the HTTP status first, then the JSON-RPC `error` member or the `google.rpc.Status` body.
- Decide on the `ErrorInfo` `reason`, never on the status code alone or on the message text.
- Retry only transport and system errors, with backoff.
- Keep the same `messageId` when you retry a send.Sources: spec §3.1.1, §3.1.6, §3.3.1, §3.3.2, §3.3.4, §5.4, §6.4, §6.5, §9.5, §10.6, §11.6 (research/sources/specification.md); docs whats-new-v1 (research/sources/docs.md); a2aproject/A2A PR #1641 and PR #1689, checked 2026-10-06; sdk src/a2a/utils/proto_utils.py, src/a2a/utils/error_handlers.py, src/a2a/server/routes/jsonrpc_dispatcher.py (a2a-python 1.2.2); sdk-go a2asrv/jsonrpc.go, a2asrv/rest.go (a2a-go v2.6.0); sdk-java reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java, transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java (a2a-java v1.4.0.Final); sdk-js src/server/express/json_rpc_handler.ts, src/server/express/rest_handler.ts (a2a-js v1.3.0); sdk-dotnet src/A2A.AspNetCore/JsonRpcResponseResult.cs, src/A2A.AspNetCore/A2AHttpProcessor.cs (a2a-dotnet v1.0.0-preview2); sdk-rust a2a-server/src/jsonrpc.rs, a2a-server/src/rest.rs (a2a-rs a2a-server-lf-v0.5.1); capture/out/11-errors.http, 16-rest-binding.http, 10-cancel.http, 14-resubscribe.http; capture/README.md