This guide covers error handling patterns, built-in error types, and best practices for managing errors in ACP applications.
Error Types
The ACP Dart SDK uses RequestError for all protocol-level errors. These errors are automatically converted to JSON-RPC error responses.
Built-in Error Codes
The SDK provides standard error constructors based on JSON-RPC 2.0 specification:
import 'package:acp_dart/acp_dart.dart';
// Parse error (-32700)
throw RequestError.parseError();
// Invalid request (-32600)
throw RequestError.invalidRequest();
// Method not found (-32601)
throw RequestError.methodNotFound('session/load');
// Invalid params (-32602)
throw RequestError.invalidParams({'reason': 'Session ID is required'});
// Internal error (-32603)
throw RequestError.internalError('Database connection failed');
// Authentication required (-32000)
throw RequestError.authRequired({'realm': 'agent-api'});
// Resource not found (-32002)
throw RequestError.resourceNotFound('/path/to/file.txt');
// Request cancelled (-32800)
throw RequestError.requestCancelled();
Agent Error Handling
Validating Request Parameters
Always validate parameters before processing:
@override
Future<PromptResponse> prompt(PromptRequest params) async {
// Validate session exists
final session = _sessions[params.sessionId];
if (session == null) {
throw RequestError.resourceNotFound(params.sessionId);
}
// Validate prompt content
if (params.prompt.isEmpty) {
throw RequestError.invalidParams({
'reason': 'Prompt cannot be empty',
});
}
// Process the prompt
// ...
}
Report tool failures through status updates:
try {
// Execute tool
final result = await executeTool(toolCall);
// Report success
await _connection.sessionUpdate(
SessionNotification(
sessionId: sessionId,
update: ToolCallUpdateSessionUpdate(
toolCallId: toolCall.id,
status: ToolCallStatus.completed,
content: [ContentToolCallContent(content: result)],
),
),
);
} catch (e) {
// Report failure
await _connection.sessionUpdate(
SessionNotification(
sessionId: sessionId,
update: ToolCallUpdateSessionUpdate(
toolCallId: toolCall.id,
status: ToolCallStatus.failed,
content: [
ContentToolCallContent(
content: TextContentBlock(
text: 'Tool execution failed: $e',
),
),
],
),
),
);
}
Handling Authentication Errors
Require authentication before allowing operations:
@override
Future<NewSessionResponse> newSession(NewSessionRequest params) async {
if (!_isAuthenticated) {
throw RequestError.authRequired({
'availableMethods': ['api_key', 'oauth'],
});
}
// Proceed with session creation
// ...
}
Graceful Cancellation
Handle cancellation requests properly:
class AgentSession {
Completer<void>? pendingPrompt;
}
@override
Future<PromptResponse> prompt(PromptRequest params) async {
final session = _sessions[params.sessionId]!;
// Set up cancellation support
final completer = Completer<void>();
session.pendingPrompt = completer;
try {
await _processPrompt(
params,
cancelStream: completer.future.asStream(),
);
return PromptResponse(stopReason: StopReason.endTurn);
} catch (e) {
// Check if cancelled
if (completer.isCompleted) {
return PromptResponse(stopReason: StopReason.cancelled);
}
rethrow;
} finally {
session.pendingPrompt = null;
}
}
@override
Future<void> cancel(CancelNotification params) async {
final session = _sessions[params.sessionId];
if (session?.pendingPrompt != null &&
!session!.pendingPrompt!.isCompleted) {
session.pendingPrompt!.complete();
}
}
Client Error Handling
Handling Connection Errors
Handle connection failures gracefully:
try {
final initResult = await connection.initialize(
InitializeRequest(
protocolVersion: 1,
clientCapabilities: ClientCapabilities(),
),
);
print('Connected successfully');
} catch (e) {
print('Failed to connect: $e');
// Retry or exit
}
Handling File System Errors
Provide meaningful errors for file operations:
@override
Future<ReadTextFileResponse> readTextFile(
ReadTextFileRequest params,
) async {
try {
final file = File(params.path);
if (!await file.exists()) {
throw RequestError.resourceNotFound(params.path);
}
final content = await file.readAsString();
return ReadTextFileResponse(content: content);
} on FileSystemException catch (e) {
throw RequestError.internalError({
'message': 'File system error',
'details': e.message,
'path': params.path,
});
} catch (e) {
throw RequestError.internalError({
'message': 'Unexpected error reading file',
'error': e.toString(),
});
}
}
@override
Future<WriteTextFileResponse> writeTextFile(
WriteTextFileRequest params,
) async {
try {
final file = File(params.path);
// Check permissions
if (await file.exists()) {
final stat = await file.stat();
if (stat.mode & 0x80 == 0) { // Not writable
throw RequestError.invalidParams({
'reason': 'File is not writable',
'path': params.path,
});
}
}
await file.writeAsString(params.content);
return WriteTextFileResponse();
} on FileSystemException catch (e) {
throw RequestError.internalError({
'message': 'Failed to write file',
'details': e.message,
});
}
}
Handling Terminal Errors
Manage terminal operation failures:
@override
Future<CreateTerminalResponse> createTerminal(
CreateTerminalRequest params,
) async {
try {
final process = await Process.start(
params.command,
params.args ?? [],
workingDirectory: params.cwd,
);
final terminalId = _generateTerminalId();
_terminals[terminalId] = process;
return CreateTerminalResponse(terminalId: terminalId);
} on ProcessException catch (e) {
throw RequestError.internalError({
'message': 'Failed to start process',
'command': params.command,
'error': e.message,
});
} catch (e) {
throw RequestError.internalError({
'message': 'Unexpected error creating terminal',
'error': e.toString(),
});
}
}
@override
Future<TerminalOutputResponse> terminalOutput(
TerminalOutputRequest params,
) async {
final process = _terminals[params.terminalId];
if (process == null) {
throw RequestError.resourceNotFound(
'Terminal ${params.terminalId} not found',
);
}
// Get output
// ...
}
Handling Permission Request Errors
Manage permission dialog failures:
@override
Future<RequestPermissionResponse> requestPermission(
RequestPermissionRequest params,
) async {
try {
// Show permission dialog to user
final choice = await _showPermissionDialog(params);
return RequestPermissionResponse(
outcome: SelectedOutcome(optionId: choice),
);
} on UserCancelledException {
return RequestPermissionResponse(
outcome: CancelledOutcome(),
);
} catch (e) {
// Log error but still return a valid response
print('Error showing permission dialog: $e');
return RequestPermissionResponse(
outcome: CancelledOutcome(),
);
}
}
Custom Error Codes
Create custom error codes for application-specific errors:
class CustomErrors {
// Custom error codes should be in the range -32000 to -32099
static RequestError sessionLimitExceeded() {
return RequestError(
-32001,
'Session limit exceeded',
{'maxSessions': 10},
);
}
static RequestError modelNotAvailable(String modelId) {
return RequestError(
-32003,
'Model not available',
{'modelId': modelId},
);
}
static RequestError rateLimitExceeded(int retryAfter) {
return RequestError(
-32004,
'Rate limit exceeded',
{'retryAfter': retryAfter},
);
}
}
// Usage
if (_sessions.length >= maxSessions) {
throw CustomErrors.sessionLimitExceeded();
}
Error Response Structure
When you throw a RequestError, it’s automatically converted to this JSON-RPC format:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32002,
"message": "Resource not found",
"data": {
"uri": "/path/to/file.txt"
}
}
}
Best Practices
Always include helpful context in error data to aid debugging.
Do:
✅ Validate inputs early and throw appropriate errors
if (params.sessionId.isEmpty) {
throw RequestError.invalidParams({'reason': 'sessionId cannot be empty'});
}
✅ Provide detailed error context
throw RequestError.resourceNotFound({
'path': params.path,
'reason': 'File does not exist or is not accessible',
});
✅ Log errors for debugging
catch (e, stackTrace) {
print('Error processing request: $e');
print(stackTrace);
throw RequestError.internalError();
}
Don’t:
❌ Expose sensitive information in errors
// Bad: exposes system paths
throw RequestError.internalError('/home/user/.secret/api_key not found');
❌ Swallow errors silently
// Bad: errors are ignored
try {
await riskyOperation();
} catch (e) {
// Silent failure
}
❌ Use generic errors without context
// Bad: no useful information
throw RequestError.internalError();
// Good: includes context
throw RequestError.internalError({
'operation': 'database query',
'error': e.toString(),
});
Next Steps
Building an Agent
Create your first ACP agent
Building a Client
Create your first ACP client