openzipkin/zipkin · error · IllegalArgumentException
spans were empty
Error message
spans were empty
What it means
SpanNode.Builder.build(spans) constructs a trace tree and refuses empty input: it immediately throws IllegalArgumentException('spans were empty'). The contract is that callers (e.g. zipkin-server's trace aggregation) pass a non-empty list belonging to one trace; an empty list is a programming or upstream data error, not a data condition to handle inside.
Source
Thrown at zipkin/src/main/java/zipkin2/internal/SpanNode.java:138
SpanNode rootSpan = null;
final Map<Object, SpanNode> keyToNode = new LinkedHashMap<>();
final Map<Object, Object> spanToParent = new LinkedHashMap<>();
void clear() {
rootSpan = null;
keyToNode.clear();
spanToParent.clear();
}
/**
* Builds a trace tree by merging and processing the input or returns an empty tree.
*
* <p>While the input can be incomplete or redundant, they must all be a part of the same trace
* (e.g. all share the same {@link Span#traceId()}).
*/
public SpanNode build(List<Span> spans) {
if (spans.isEmpty()) throw new IllegalArgumentException("spans were empty");
clear();
// In order to make a tree, we need clean data. This will merge any duplicates so that we
// don't have redundant leaves on the tree.
List<Span> cleaned = Trace.merge(spans);
int length = cleaned.size();
String traceId = cleaned.get(0).traceId();
if (logger.isLoggable(FINE)) logger.fine("building trace tree: traceId=" + traceId);
// Next, index all the spans so that we can understand any relationships.
for (int i = 0; i < length; i++) {
index(cleaned.get(i));
}
// Now that we've index references to all spans, we can revise any parent-child relationships.
// Notably, by now, we can tell which is the root-most.
for (int i = 0; i < length; i++) {View on GitHub (pinned to 878ce2a1fa)
Solutions
- Guard the call: skip tree building when spans.isEmpty() — there is no tree to build.
- If empty lists surprise you, log upstream: find the filter/map stage that dropped every span for that trace ID.
- In tests, pass at least one real Span built via Span.newBuilder().traceId(...).id(...).build().
Example fix
// before SpanNode tree = new SpanNode.Builder().build(spansByTraceId.get(traceId)); // after List<Span> spans = spansByTraceId.get(traceId); if (spans == null || spans.isEmpty()) continue; SpanNode tree = new SpanNode.Builder().build(spans);
Defensive patterns
Strategy: validation
Validate before calling
if (spans == null || spans.isEmpty()) return SpanNode.newBuilder().build(); // or skip entirely
Type guard
boolean hasSpans(List<Span> spans) { return spans != null && !spans.isEmpty(); } Prevention
- Check isEmpty() before build() at every call site.
- Log when a trace group is empty — it usually signals an over-aggressive filter upstream.
When it happens
Trigger: Calling new SpanNode.Builder().build(spans) with Collections.emptyList() — e.g. an aggregation stage that groups spans by trace ID and blindly forwards every group, including empty ones, or a query path that found no spans.
Common situations: Custom trace-correlation code streaming spans from multiple sources; tests passing empty fixtures; race conditions where a trace's spans were all filtered out before tree building.
Related errors
- input is not a list
- List<Span> implies at least traceId and id fields
- v1 format is not supported. For help, contact https://gitter
- concurrency < 1
- rate should be between 0 and 1: was {}
AI-assisted analysis of openzipkin/zipkin@878ce2a1fa (2026-08-14).
Data as JSON: /api/errors/3e72828196c6c0fd.
Report an issue: GitHub.