Skip to content
1 min read · 261 words

Errors during dispatch

How the four error sources during a dispatch reach their terminal state, and why the surfacing differs depending on whether dispatch() was called by a TurnRunner or standalone.

LLM Dispatch covers the dispatch contract; the Exception Reference lists every dispatch-specific E_* code under Dispatch.

Error sources

  • Executor throws (non-abort). Wrapped as E_LLM_EXECUTION_EXECUTOR_ERROR, emitted on error, the pending delta queue is cleared, the dispatch nacks.
  • Input or output middleware throws (non-abort). Both pipelines surface their failures through the same code, E_DISPATCH_PIPELINE_ERROR. Emitted on error, the dispatch nacks. (The summary names "input pipeline" and "output pipeline" are not encoded in the exception itself — observers identify the side via iterationStart / iterationEnd framing or via a label inside the middleware.)
  • Abort. The AbortSignal fires (turn-level abort, gate-aborted, or whatever else is wired into the controller). The delta queue is discarded, the loop breaks, dispatchEnd.status is 'aborted'. No error event is emitted — abort is not an error.
  • ctx.nack(error) called. dispatchEnd.status is 'nack', dispatchEnd.error is the supplied error.

Where errors surface depends on the entry point

When TurnRunner.run() is the caller, dispatch errors are caught by the runner, emitted on the error bus, and run() still resolves. When dispatch() is called standalone, the same errors reject the dispatch() promise — no TurnRunner is there to swallow them. Wire your own try/catch around standalone dispatches, or rely on the observability hooks you passed in.

See the Exception Reference for the exception codes and what each one means about which seam misbehaved.