{"record":{"id":"3e72828196c6c0fd","repo":"openzipkin/zipkin","slug":"spans-were-empty","errorCode":null,"errorMessage":"spans were empty","messagePattern":"spans were empty","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"zipkin/src/main/java/zipkin2/internal/SpanNode.java","lineNumber":138,"sourceCode":"\n    SpanNode rootSpan = null;\n    final Map<Object, SpanNode> keyToNode = new LinkedHashMap<>();\n    final Map<Object, Object> spanToParent = new LinkedHashMap<>();\n\n    void clear() {\n      rootSpan = null;\n      keyToNode.clear();\n      spanToParent.clear();\n    }\n\n    /**\n     * Builds a trace tree by merging and processing the input or returns an empty tree.\n     *\n     * <p>While the input can be incomplete or redundant, they must all be a part of the same trace\n     * (e.g. all share the same {@link Span#traceId()}).\n     */\n    public SpanNode build(List<Span> spans) {\n      if (spans.isEmpty()) throw new IllegalArgumentException(\"spans were empty\");\n      clear();\n\n      // In order to make a tree, we need clean data. This will merge any duplicates so that we\n      // don't have redundant leaves on the tree.\n      List<Span> cleaned = Trace.merge(spans);\n      int length = cleaned.size();\n      String traceId = cleaned.get(0).traceId();\n\n      if (logger.isLoggable(FINE)) logger.fine(\"building trace tree: traceId=\" + traceId);\n\n      // Next, index all the spans so that we can understand any relationships.\n      for (int i = 0; i < length; i++) {\n        index(cleaned.get(i));\n      }\n\n      // Now that we've index references to all spans, we can revise any parent-child relationships.\n      // Notably, by now, we can tell which is the root-most.\n      for (int i = 0; i < length; i++) {","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/openzipkin/zipkin/blob/878ce2a1fad54ca941d17fdcf2e1d924b148eb1f/zipkin/src/main/java/zipkin2/internal/SpanNode.java#L120-L156","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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()."],"exampleFix":"// before\nSpanNode tree = new SpanNode.Builder().build(spansByTraceId.get(traceId));\n\n// after\nList<Span> spans = spansByTraceId.get(traceId);\nif (spans == null || spans.isEmpty()) continue;\nSpanNode tree = new SpanNode.Builder().build(spans);","handlingStrategy":"validation","validationCode":"if (spans == null || spans.isEmpty()) return SpanNode.newBuilder().build(); // or skip entirely","typeGuard":"boolean hasSpans(List<Span> spans) { return spans != null && !spans.isEmpty(); }","tryCatchPattern":null,"preventionTips":["Check isEmpty() before build() at every call site.","Log when a trace group is empty — it usually signals an over-aggressive filter upstream."],"tags":["zipkin","trace-tree","validation"],"backgroundTag":null,"analyzedSha":"878ce2a1fad54ca941d17fdcf2e1d924b148eb1f","analyzedAt":"2026-08-14T15:17:09.895Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}