Skip to content

Agent error handling

Genkit agent failures have different recovery paths depending on when they happen. A request can be rejected before an invocation starts, a turn can fail after state has been loaded, or a tool can return a domain-level result that the model can handle.

  • Init misuse throws an immediate exception (e.g., AgentInitError or GenkitException) before a turn starts. Fix the parameters, such as by not sending state to a store-backed agent.
  • Failed turns throw AgentError containing status, details, the latest snapshot ID (err.snapshotId), and the recoverable last-good state (err.response.raw.state holds the full SessionState; err.state holds the typed custom state).
  • Aborted turns (a cancelled request or hitting maxTurns) resolve to a response with finishReason: AgentFinishReason.aborted rather than throwing AgentError, and also commit a rerunnable snapshot (res.snapshotId) with the last-good state.
  • Background/detached failures appear as snapshot status failed, aborted, or expired. Inspect the snapshot’s error property and rerun from it.
  • Tool domain problems should return structured tool outputs when the model can recover. Let the model explain the issue or ask the user for corrected input.

A failed or aborted turn commits a rerunnable snapshot on server-managed agents: it carries the structured error (on failure) plus the last-good history, ending before the turn that failed or aborted. Resume that snapshot with chat(snapshotId: ...), then send an empty message to re-drive the same turn, or send new text to steer the retry.

try {
final res = await chat.send(text: 'Look up order 123.');
if (res.finishReason == AgentFinishReason.aborted && res.snapshotId != null) {
// Aborted turn (cancelled or hit maxTurns): rerun from the committed snapshot.
final rerun = agent.chat(snapshotId: res.snapshotId);
await rerun.send(); // or: rerun.send(text: 'Try a shorter summary.')
} else {
print(res.text);
}
} on AgentError catch (err) {
print('Turn failed: ${err.status}');
print('Error details: ${err.message}');
if (err.snapshotId != null) {
// Server-managed agent: rerun the failed snapshot. send() with no message
// re-drives the same turn; pass text to steer the retry.
final rerun = agent.chat(snapshotId: err.snapshotId);
await rerun.send(); // or: rerun.send(text: 'Try again with order 456.')
} else {
// Client-managed agent: the existing `chat` instance automatically rolls
// back the failed turn's user message, or you can construct a fresh chat
// from the complete SessionState (`err.response.raw.state`).
final recoveryChat = agent.chat(state: err.response.raw.state);
await recoveryChat.send(text: 'Try again with order 456.');
}
}

For streaming turns, catch errors around the chunk stream consumption and the final response Future. The stream rethrows failed-turn errors after yielding any chunks that arrived before the failure occurred.

final turn = chat.sendStream(text: 'Write a long report.');
try {
await for (final chunk in turn.stream) {
render(chunk);
}
await turn.response;
} on AgentError catch (err) {
showFailure(err);
}

Throw an exception from a tool when the system cannot safely proceed, such as a database outage or auth failure:

final lookupOrder = ai.defineTool(
name: 'lookupOrder',
description: 'Looks up an order by ID.',
inputSchema: LookupOrderInput.$schema,
outputSchema: Order.$schema,
fn: (input, _) async {
final order = await db.orders.find(input.orderId);
if (order == null) {
throw Exception('Order ${input.orderId} was not found.');
}
return order;
},
);

When the error is soft and the model has a chance to recover (e.g. invalid user input), return structured output:

if (order == null) {
return {
'ok': false,
'reason': 'ORDER_NOT_FOUND',
'message': 'Ask the user to check the order ID.',
};
}