Error handling

An agent meets errors from three places: its own expressions, the systems it calls, and the limits the runtime enforces. A command that calls out can handle its own errors in an onError block, a function can handle what its commands did not, and whatever is not handled ends the run with a result code, a message for the person and a record in the error report. Nothing from a failed run is saved.

Where errors come from

  • Expressions. A name that is not set, a missing dict key, an index out of range, a wrong type. The message names the command's path and the Python error: Could not evaluate the Python expression at main.commands.var: 'phone'. A field that evaluates to the wrong type is refused the same way: The expression at prompts.chat.system is expected to evaluate to str but it evaluated to int.
  • api. A path, header or query parameter the API profile does not allow, a timeout, or a connection failure. The command does not check the HTTP status code: a 404 or a 500 is a result for the next command to test.
  • db. An unknown profile, a timeout, a connection or SQL error, or more rows than the account's row limit.
  • llm. A vendor error that survives the retries, a request that cannot be built, more tools than the profile allows, or an answer of the wrong shape for structured output.
  • Loops. A for or while that reaches the account's max loop iterations setting.
  • Functions. A function called again while it is still running.
  • Hooks. A hook that raises.

Validation catches many of these before the agent runs: a variable nothing sets, a prompt the agent does not have, a const key the const does not have, a function that is not on the allowlist. What remains depends on the data the agent meets at run time.

Three commands end a run on purpose and are not errors: ask pauses the run, end finishes it, and fail fails it with a message of the agent's own. None of them can be caught.

onError on a command

api, db and llm accept an onError block. When the command raises, exc is set to the exception, the block's commands run, and the function continues with the command after the failed one. The failed command produced no result, so the block sets a variable the rest of the function can rely on:

"api": {
  "profile": "weather",
  "path": "{ f'/forecast/{city}' }",
  "onError": {
    "log": "{ f'Forecast unavailable for {city}: {exc}' }",
    "var": { "forecastError": "{ str(exc) }" }
  }
},
"if": {
  "condition": "{ sys.exists('local', 'forecastError') }",
  "then": { "output": "{ f'No forecast for {city} right now: {forecastError}' }" },
  "else": { "var": { "forecast": "{ result }" } }
}

Inside the block:

  • exc is the exception. { exc } in an f-string gives its message.
  • result is whatever the last command before the failed one produced, not a result of the failed command. Do not rely on it.
  • ask is not allowed, because a run cannot pause while it handles an error. Set a variable in the block and ask after the command, with an if on that variable.
  • A command in the block can itself fail. That error goes to the function's onError.

onError on a function

A function can carry an onError block of its own. It runs when an error leaves one of the function's commands: a command without an onError, a command whose onError failed, or a command that does not accept one, such as var, for or func. The rest of the function is skipped. With a return in the block, the returned value becomes the function's result. Without one the function returns nothing, and the caller continues.

"lookupOrder": {
  "args": { "orderId": {} },
  "commands": {
    "api": { "profile": "orders", "path": "{ f'/orders/{orderId}' }" },
    "return": "{ result }"
  },
  "onError": {
    "log": "{ f'Order lookup failed for {orderId}: {exc}' }",
    "return": "unknown"
  }
}

A function can fail before any of its commands has run, so its onError block sees only the function's arguments, what the block itself sets, exc and result. Validation refuses an expression in the block that reads a variable from the function body.

main is a function too. An error that reaches the onError of main and is handled there ends the run normally, with whatever output the agent wrote.

What cannot be caught

  • The agent's cost limit. Reaching it ends the run at once, inside an onError, a tool call or a parallel branch alike.
  • The agent's time limit. The run is ended from outside with the result code timedOut.
  • fail, a stop by the person, and an error from Search2o Cloud.
  • A hook that refuses the run.

Errors in tools

A function the model calls as a tool handles errors like any other function: its commands' onError blocks and its own onError apply. An error that leaves the function is not given to the model. It ends the run, with the model's answer never produced. A tool that should let the model recover catches the error itself and returns a value that says what went wrong.

A tool on an MCP server is different. An error the server reports as a tool error, or a JSON-RPC error whose code the MCP profile lists as retryable, goes back to the model together with the profile's tool error prompt, and the model can correct its arguments or give up. Any other MCP error ends the run.

fail inside a tool ends the run with its message. end is not allowed in a tool. ask pauses the run, and the model's tool loop resumes when the person answers.

Errors in parallel branches

When a branch of a parallel command fails, the command fails with that error, and the function's onError can handle it. When several branches fail, their messages are combined into one. A stop, a cost limit or an ask in any branch takes precedence over the failures of the others, which the trace still records.

What the person sees

An error that nothing handled ends the run with the result code errorInAgent. The person reads This agent could not complete your request. The problem has been reported to its developer. When the cause was a timeout or a connection failure on an API, a database or an LLM, the person reads The agent could not reach a service it needs. Please try again in a moment. A fail shows its own message. A run past the time limit or the cost limit says which limit it reached.

The conversation state is not saved after a failed run. A follow-up in the same conversation sees the variables, prompts and output as they were before the run.

Where to look

  • During validation, the trace shows every error with the path of the command it happened at, including the ones an onError block handled.
  • In production, the error report groups each agent's failures by version, result code and message, with the command path. The result codes say which kind of failure each was.
  • The onAgentError hook receives the result code, the message and the path of every failed run, for alerting from your own code.