Status & errors
How run and span status work, and how errors are captured with a type, HTTP status code, stack trace, and severity level.
Every run and span carries a status, and errors are how a span or run becomes error. Most of the time it's automatic: a thrown exception inside the wrap is captured for you, including the error type, message, HTTP/provider status code, and stack trace. You can also record errors and set severity explicitly.
Status
| Level | Status values | How it's set |
|---|---|---|
| Run | ok · error · running | running until the run ends, then ok, or error on a thrown callback or setErrorSummary |
| Span | ok · error | ok on success, error on a thrown callback or setError |
A run stays running until wrapAgent returns (or endRun is called), and rolls up an error_count from its spans.
Severity level
Alongside status, every span and run carries a level, a severity gradient:
| Level | Use for |
|---|---|
debug | Verbose/diagnostic spans |
default | Normal spans (the implicit default) |
warning | A recovered or retried step, notable but not a failure |
error | A failure |
level is derived automatically (error on a failure, otherwise default), so you never have to set it. Set it explicitly only to flag a warning/debug without changing the ok/error status:
await trodo.withSpan('fetch', async (span) => {
span.setTool('fetch');
const res = await fetchWithRetry(url); // succeeded, but only after a retry
span.setLevel('warning'); // status stays 'ok'
return res;
}, { kind: 'tool' });with trodo.span('fetch', kind='tool') as span:
res = fetch_with_retry(url) # succeeded, but only after a retry
span.set_level('warning') # status stays 'ok'Errors are captured automatically
A thrown exception inside wrapAgent, a span helper, or withSpan is recorded and re-raised. The failing span gets status='error', level='error', and, parsed from the error object, error_type, error_message, status_code and stack_trace. The run's error_count increments, and if the throw bubbles to the run, the run is marked error too. You don't need a try/catch just to capture it.
await trodo.wrapAgent('agent', async (run) => {
// If the OpenAI/Anthropic call throws a rate-limit error, the span records
// error_type='RateLimitError', status_code='429', the message, and the stack.
return await agent.run(query);
});with trodo.wrap_agent('agent') as run:
# If the provider call raises a rate-limit error, the span records
# error_type='RateLimitError', status_code='429', the message, and traceback.
return agent.run(query)The status code is read from the thrown error's status / status_code / response.status_code / code, covering OpenAI, Anthropic, httpx/requests, fetch/axios, and stdlib errors. Auto-instrumented provider spans (OpenAI, Anthropic, LangChain, …) surface the same fields from their OpenTelemetry exception events.
Record an error manually
When you catch an error yourself, to recover or to add detail, set it on the span. This finalises the span as error without re-throwing.
await trodo.withSpan('charge', async (span) => {
span.setTool('charge');
try {
return await billing.charge(id);
} catch (err) {
span.setError({
type: 'ChargeFailed',
message: err.message,
statusCode: err.status, // optional: HTTP/provider code
});
return fallbackReceipt(); // recover; span is still 'error'
}
}, { kind: 'tool' });with trodo.span('charge', kind='tool') as span:
try:
return billing.charge(id)
except Exception as e:
span.set_error(
str(e),
type='ChargeFailed',
status_code=getattr(e, 'status', None), # optional
)
return fallback_receipt() # recover; span is still 'error'For a run-level summary, set it on the run or pass it to endRun:
await trodo.wrapAgent('agent', async (run) => {
if (!ok) run.setErrorSummary('No candidates returned', { type: 'NoCandidates' });
});
// long-running runs:
await trodo.endRun(runId, { status: 'error', errorSummary: 'timed out' });with trodo.wrap_agent('agent') as run:
if not ok:
run.set_error_summary('No candidates returned', type='NoCandidates')
# long-running runs:
trodo.end_run(run_id, status='error', error_summary='timed out')Two levels of error
| Level | Trigger | Sets |
|---|---|---|
| Span error | A helper or withSpan callback throws, or setError | status='error', level='error', error_type, error_message, status_code, stack_trace; increments the run's error_count |
| Run error | The wrapAgent callback throws, or setErrorSummary | status='error', level='error', error_summary, error_type on the run |
A throw inside a nested span that bubbles all the way up sets both.
Where errors show up
Every failed run and span lands on the Failures tab (Assess → Issues, where it opens), grouped by agent, tool and error, and filed into a category from its type, status code and message: timeout, rate limited, auth, not found, refused, bad request, server error, connection, code error or other. A failed parent only repeats its child's error, so a failure is counted once, at the deepest span that failed. The same categories are a filter on alerts.
Sending status_code and a real error_type is what puts a failure in the right category.
Errors that don't throw
Some providers return HTTP 200 with an error in the body. You have two choices:
- Record it explicitly with
setError(Node) /set_error(Python): the span becomes an error and counts towarderror_count, without you re-throwing. - Keep
status='ok'and tag it withsetAttribute('llm_error', …), so it stays filterable without inflating the error count. OptionallysetLevel('warning')to flag it.
Next
- Add manual spans for the span API
- Metadata for attributes like
llm_error