Skip to content

Errors

Five exception types, all sealed under AhpException. The distinction they draw is the one that matters operationally: whether retrying could possibly help.

sealed class AhpException implements Exception {
final String message;
}
Type Raised when Retry?
AhpRpcException The host answered with a JSON-RPC error Depends on code
AhpTimeoutException A request got no answer in time Yes
AhpTransportException The socket failed to open or dropped Yes, with backoff
AhpClosedException A request was made on a closed connection No — reconnect first
AhpDecodeException A payload did not match the schema No — a bug, somewhere

Matching is exhaustive:

switch (error) {
case AhpRpcException(:final code, :final data):
case AhpTimeoutException():
case AhpTransportException(:final cause):
case AhpClosedException():
case AhpDecodeException(:final type, :final field):
}
if (e.code == AhpErrorCodes.unsupportedProtocolVersion) { /* ... */ }
Code Constant
-32001 sessionNotFound
-32002 providerNotFound
-32003 sessionAlreadyExists
-32004 turnInProgress
-32005 unsupportedProtocolVersion
-32006 contentNotFound
-32007 authRequired
-32008 notFound
-32009 permissionDenied
-32010 alreadyExists
-32011 conflict

The standard JSON-RPC codes live in JsonRpcErrorCodes.

Some codes attach a typed payload:

if (e.code == AhpErrorCodes.unsupportedProtocolVersion) {
final data = e.data;
if (data is Map<String, Object?>) {
print('host speaks: ${data['supportedVersions']}');
print('we offered: $supportedProtocolVersions');
}
}

unsupportedProtocolVersion is terminal, and it is the clearest example of why the retry column above matters. The client already offered every version it can speak — there is nothing left to degrade to, and a retry loop will fail identically forever. The fix is a newer client.

The payload has a generated type, so it is worth decoding rather than reaching into the map:

on AhpRpcException catch (e) {
if (e.code == AhpErrorCodes.unsupportedProtocolVersion) {
final data = UnsupportedProtocolVersionErrorData.fromJson(
e.data! as Map<String, Object?>,
);
print('host speaks ${data.supportedVersions}');
}
}

Note those may be SemVer ranges rather than exact versions — a real host answers ^0.7.0, so comparing them for equality against supportedProtocolVersions will not do what you want.

authRequired carries AuthRequiredErrorData, and permissionDenied carries PermissionDeniedErrorData.

AhpDecodeException names the type and the field:

AhpDecodeException: ChatState.activeTurn: required field missing

That specificity is deliberate. Without it, a missing required field surfaces as type 'Null' is not a subtype of type 'String' from somewhere inside a generated constructor, which tells you nothing about which payload was wrong.

Seeing one means the host sent something the schema does not describe. Either the vendored schema is behind the host, or the host has a bug. It is not a condition to handle at runtime — it is one to report.

A type this client does not recognize is not a decode failure. It decodes to the union’s Unknown variant, keeps the raw JSON, and re-encodes unchanged. The specification requires clients to ignore what they do not understand, and reducers treat those as no-ops. See Actions.