Skip to content
2 min read · 406 words

Failure

ADK validates eagerly, names every exception with a stable error code, and does not swallow errors. The runner's behavior on failure is mechanical: validation throws at the seam where the bad input arrived; pipeline failures surface as error events on the observability bus; dispatch failures end the dispatch with dispatchEnd.status: 'nack'; abort short-circuits silently.

The per-code reference — every E_* code organized by seam, with fatality, behavior, and the nuances that matter in production — lives in the Exception Reference.

Exception anchors

Every exception is constructed by createException. The code is the stable identifier (E_*). The fatal flag controls whether the runner throws out of run() or emits on the error bus.

Fatal vs non-fatal is a structural distinction

  • Fatal exceptions indicate programming errors. They throw synchronously at the seam where the bad input arrived. There is no path back into the runner.
  • Non-fatal exceptions indicate runtime failures. They are emitted on the observability error event and the pipeline stage that produced them is skipped; the turn / dispatch continues to its terminal event.

Abort is not an error

Abort is a settlement outcome, not a failure

When the turn-level abort signal fires (or middleware throws an AbortError), the pipeline short-circuits silently:

  • No error event is emitted.
  • turnEnd still fires.
  • dispatchEnd.status is 'aborted' (if the abort occurred during dispatch).
  • Pending deltas inside dispatch are discarded — partial writes never reach storage.

The consumer's abort handler is responsible for any user-visible signal. The runner does not interpret abort as failure; abort is a settlement outcome on its own.

What you do not catch

try/catch around run() is not enough

run() resolves with void and rejects only with the fatal exceptions listed above (validation errors at construction and entry). Everything else surfaces through events:

  • Non-fatal pipeline errors → runner.observe('error', ...).
  • Dispatch settlement → runner.observe('dispatchEnd', ev => ev.status === 'nack' ? ev.error : ...).
  • Tool errors → either toolExecutionEnd (with isError: true) on the observability bus, or toolCall (with isError: true) on the functional bus.
  • Gate failures → the TurnContext.waitFor promise rejects; observability sees turnGateClosed with the result.

If you wrap run() in a try { } catch { }, you are catching programmer errors (invalid config, invalid context). For runtime failures, listen to events.