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.
Failure categories
Section titled “Failure categories”- Init misuse throws before a turn starts. Fix the caller, such as by not sending
stateto a store-backed agent. - Failed turns throw
AgentErrorwithresponse,status,details,state, andsnapshotId. The turn keeps the conversation up to its last completed tool round, so you can retry from there (see Resume after a failure or a stop). - Foreground aborts resolve with
finishReason: 'aborted'. An in-process chat moves to the resume point the stop left; aremoteAgentchat stays at its previous snapshot (see Resume after a failure or a stop). Let the user retry or revise the request. - Background failures appear as snapshot status
failed,aborted, orexpired. Show the status, inspect snapshot error details, and resume from thefailedorabortedsnapshot. - Domain-level tool problems should be structured tool output when the model can recover. Let the model explain the issue or ask the user for corrected input.
Handle failed turns
Section titled “Handle failed turns”import { AgentError } from 'genkit/beta/client';
try { const res = await chat.send('Look up order 123.'); console.log(res.text);} catch (err) { if (err instanceof AgentError) { console.error(err.status); console.error(err.details); console.error(err.snapshotId); console.error(err.state);
const recoveryChat = err.snapshotId ? await agent.loadChat({ snapshotId: err.snapshotId }) : agent.chat({ state: err.response.raw.state });
await recoveryChat.send('Try again with order 456.'); } else { throw err; }}For streaming turns, catch errors around stream consumption and the final response. The stream rethrows failed-turn errors after yielding any available chunks.
const turn = chat.sendStream('Write a report.');
try { for await (const chunk of turn.stream) { render(chunk); }
await turn.response;} catch (err) { showFailure(err);}Resume after a failure or a stop
Section titled “Resume after a failure or a stop”A failed turn commits the conversation it had at the last turn seam: the messages the turn started from plus every completed round of tool requests and tool responses. With a session store, that conversation is saved as a failed snapshot. A run the caller stopped (through an abort signal, a timeout, or maxTurns) is saved the same way as an aborted snapshot. Both are resume points, so a retry does not repeat the tool calls that already finished.
Send an input with neither a message nor a resume to run the turn again on the committed conversation. Only a turn that committed can be re-run this way. A failure before the turn committed anything leaves the chat at its previous resume point without the new message, so send the message again instead. With a session store, a committed turn is one that moved the snapshot ID. Open an existing session with loadChat() rather than chat({ sessionId }), so chat.snapshotId is set before the turn:
const before = chat.snapshotId;try { await chat.send('Book the full itinerary.');} catch (err) { if (!(err instanceof AgentError)) throw err; if (err.snapshotId && err.snapshotId !== before) { // The turn committed: run it again from where it failed. await chat.send({}); } else { // Nothing was committed: send the message again. await chat.send('Book the full itinerary.'); }}A client-managed agent has no snapshot ID. A model or tool failure usually commits there too, and the chat adopts the committed state, so check whether err.response.raw.state?.messages grew past what the chat had before the turn.
An attached run that you stop resolves instead of throwing. For an in-process agent, such as one from ai.defineAgent(), the chat adopts the snapshot the stop left:
const ac = new AbortController();const res = await chat.send('Book it.', { abortSignal: ac.signal }); // ac.abort() elsewhere
if (res.finishReason === 'aborted') { // chat.snapshotId names where the run stopped. await chat.send({});}With remoteAgent, stopping the run cancels the HTTP request, so the chat never sees how the run ended and stays at its previous snapshot. Reload the session’s latest snapshot with agent.loadChat({ sessionId }) before you retry.
From another process, load the failed or aborted snapshot by ID and continue:
// The err.snapshotId or chat.snapshotId you saved earlier.const chat = await agent.loadChat({ snapshotId: savedSnapshotId });await chat.send('Try again, but skip the hotel.');A few rules worth knowing:
- A failure that happens before the turn commits anything rolls back. No snapshot records it, and the previous committed snapshot stays the resume point.
err.statusis the model’s own status for a model error andINTERNALfor a failed tool.- An empty input on a fresh conversation is rejected with
INVALID_ARGUMENT, because there is nothing to re-run. pendingandabortingsnapshots cannot be resumed yet. Wait for them to settle first.- A custom agent built with
defineCustomAgentcommits a failed turn only when its turn handler throwsCommittedTurnError; see Failure and recovery. It receives the empty input as is, and has to handle it.
The same behavior is available on a single generate() call with throwOnError: false; see Error types.
Tool exceptions
Section titled “Tool exceptions”Throw from a tool when the system cannot safely continue, such as a database outage, auth failure, or invariant violation.
const lookupOrder = ai.defineTool( { name: 'lookupOrder', description: 'Looks up an order by ID.', inputSchema: z.object({ orderId: z.string() }), }, async ({ orderId }) => { const order = await db.orders.find(orderId); if (!order) { throw new Error(`Order ${orderId} was not found.`); } return order; },);Return structured data when the model can recover:
return { ok: false, reason: 'ORDER_NOT_FOUND', message: 'Ask the user to check the order ID.',};Response validation
Section titled “Response validation”Call res.assertValid() when a caller requires a model message and wants blocked responses to throw.
const res = await chat.send('Write the summary.');res.assertValid();