Skip to content

[Feat]: Unified A2AError hierarchy with transport-specific subclasses #583

Description

@JakubWorek

Problem

SDK errors are ~11 disjoint Error subclasses (plus v0.3's separate A2AError) wired together by 6+ parallel error.name-keyed lookup tables and duplicated instanceof chains in each transport. Consequences:

  • No shared base — client and server rebuild the same mappings.
  • Transport-native context is lost: HTTP status/headers, cause, and gRPC grpc-status-details-bin metadata never reach the caller (see [Feat]: Passthrough http status code in error #317).
  • Adding an error class means touching 6+ tables and 3 mapping chains.

The two dimensions callers need to discriminate on — what went wrong (semantic) and which wire carried it (transport) — are orthogonal. JS single-inheritance means a class tree has to pick one dimension as parent. The proposal below picks semantic as the class axis and models transport as interfaces + user-defined type guards, so both dimensions remain reachable.

Proposal

Class axis is semantic. A2AError is the shared base; each spec-aligned error extends it; each transport publishes a concrete per-transport subclass of every semantic error. Transport-native context is expressed as an interface implemented by all subclasses of a given transport, and narrowed at catch time via user-defined type guards (isRestError, isGrpcError, isJsonRpcError).

flowchart TD
    Error --> A2AError

    A2AError --> TNF[TaskNotFoundError]
    A2AError --> UOE[UnsupportedOperationError]
    A2AError --> Etc["…8 more semantic errors"]

    TNF --> RTNF[RestTaskNotFoundError]
    TNF --> GTNF[GrpcTaskNotFoundError]
    TNF --> JTNF[JsonRpcTaskNotFoundError]

    RE[[RestA2AError<br/><i>interface</i>]] -.implements.-> RTNF
    GE[[GrpcA2AError<br/><i>interface</i>]] -.implements.-> GTNF
    JE[[JsonRpcA2AError<br/><i>interface</i>]] -.implements.-> JTNF
Loading
  • A2AError — extends Error; carries spec-aligned reason/code and structured metadata. Base for all semantic errors.
  • Semantic errors (TaskNotFoundError, UnsupportedOperationError, TaskNotCancelableError, ContentTypeNotSupportedError, InvalidAgentResponseError, PushNotificationNotSupportedError, ExtendedAgentCardNotConfiguredError, VersionNotSupportedError, RequestMalformedError, ExtensionSupportRequiredError) — extend A2AError; carry error-specific fields (e.g., taskId).
  • Per-transport concrete subclasses (RestTaskNotFoundError, GrpcTaskNotFoundError, JsonRpcTaskNotFoundError, and analogues for every semantic error) — extend the corresponding semantic class and implement the transport interface.
  • Transport interfaces (RestA2AError, GrpcA2AError, JsonRpcA2AError) — declare transport-native context:
    • RestA2AError: statusCode, headers, cause
    • GrpcA2AError: status, statusDetailsBin (no cause on the wire)
    • JsonRpcA2AError: envelopeCode, data
  • Type guards (isRestError(e), isGrpcError(e), isJsonRpcError(e)) — user-defined type predicates that narrow an A2AError to the transport interface.

Catch-site ergonomics

try {
  await client.getTask({ id });
} catch (e) {
  if (e instanceof TaskNotFoundError) {
    // Transport-agnostic: e.taskId is available.
    if (isRestError(e)) {
      // Narrowed: e.statusCode, e.headers, e.cause are typed.
      if (e.statusCode === 429) backoff(e.headers['retry-after']);
    }
  }
}

The TS compiler intersects the class type with the interface type inside the guard, so both semantic fields (taskId) and transport fields (statusCode) are simultaneously accessible.

Closes #317.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions