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 that mirrors Langfuse:
| 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.
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