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

  1. Guard the call: skip tree building when spans.isEmpty() — there is no tree to build.
  2. If empty lists surprise you, log upstream: find the filter/map stage that dropped every span for that trace ID.
  3. 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

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


AI-assisted analysis of openzipkin/zipkin@878ce2a1fa (2026-08-14). Data as JSON: /api/errors/3e72828196c6c0fd. Report an issue: GitHub.