Skip to content

# startActiveSpan silently fails when callback returns void - causing spans to be lost #5905

@arthikek

Description

@arthikek

OpenTelemetry: startActiveSpan silently fails when callback returns void

This document describes an observed issue where OpenTelemetry spans may be lost when using startActiveSpan callbacks that do not explicitly return a value. It includes reproduction steps, environment details, impact, root cause analysis, and suggested mitigations.

Summary

When the callback passed to startActiveSpan returns void/undefined (commonly in async Express handlers that don’t return a response), spans may not be exported, and no warnings or errors are emitted. Returning a value (or properly awaiting and ending the span) avoids the issue.

Environment

  • Runtime: Node.js 24.7.0, ESM (package.json: "type": "module")
  • OpenTelemetry packages (representative versions):
    • @opentelemetry/api: ^1.9.0
    • @opentelemetry/sdk-node: ^0.200.0
    • @opentelemetry/auto-instrumentations-node: ^0.57.0
    • @opentelemetry/instrumentation-winston: ^0.48.1
    • @opentelemetry/sdk-metrics: ^2.0.0
  • Platform: Google Cloud Platform (staging)

Expected behavior

One of the following should be true:

  • Spans are exported regardless of the callback’s return value; or
  • Documentation clearly states callbacks MUST return a value; or
  • A runtime warning is emitted when the callback returns undefined.

Actual behavior

  • Spans can be silently lost when the callback returns void/undefined.
  • No warnings or errors are logged.

Reproduction

Working (spans appear):

app.get("/working", (req, res) => {
  const tracer = trace.getTracer("test-service");
  return tracer.startActiveSpan("test-operation", (span) => {
    span.setAttributes({ "http.method": req.method });
    span.end();
    return res.json({ success: true }); // ✅ RETURN here
  });
});

Broken (spans missing):

  app.get("/old", async (req, res) => {
    const tracer = trace.getTracer("dice-service-arthike");
    await tracer.startActiveSpan("roll-dice-arthike", async (span) => {
      try {
        span.setAttributes({
          "dice.endpoint": "/roll-dice-new",
          "http.method": req.method,
          "http.url": req.url,
          "arthike.custom": "debugging-span"
        });
        const result = [1, 2, 5];
        span.setAttributes({
          "dice.result_count": result.length,
          "dice.result_sum": result.reduce((sum, val) => sum + val, 0),
          "http.status_code": 200
        });
        span.setStatus({ code: SpanStatusCode.OK });
        res.json(result);
      } catch (error) {
        span.recordException(error as Error);
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: "Internal error"
        });
        res.status(500).send("Internal server error");
      } finally {
        span.end();
      }
    });
  });

Notes:

  • In the “broken” example, the async callback implicitly returns Promise<void>.
  • Depending on the processor and exporter, spans may be dropped or not flushed as expected.

Impact

  • Silent failure: Application behaves correctly but observability data is missing.
  • Difficult debugging: No warnings/errors indicate the problem.
  • Common pattern: Many Express handlers don’t explicitly return a response.
  • Production risk: Missing traces in production environments.

Root cause analysis (high-level)

  • startActiveSpan manages context and span lifecycle around a callback. Tooling and type inference may treat Promise<void> callbacks in a way that ends or flushes spans differently if the return value isn’t propagated or awaited as expected.
  • If the callback returns undefined, some span lifecycle steps may not be properly coordinated with the surrounding context, resulting in spans that are not exported or are dropped.

Suggested solutions (short and long term)

Short-term mitigations you can apply now:

  • Always return a value from the startActiveSpan callback, even in async handlers:
    await tracer.startActiveSpan("op", async (span) => {
      try {
        // ... work ...
        return true; // or res.json(...)
      } finally {
        span.end();
      }
    });
  • Prefer try/finally to ensure span.end() always runs.
  • Ensure the returned promise is awaited by the caller so the span lifecycle is tied to completion.

Medium/long-term improvement ideas for the codebase or upstream:

  • Documentation: Clearly state that callbacks SHOULD return a value and be awaited.
  • Runtime warning: Detect undefined return and log a warning in development.
  • API robustness: Make span export independent of callback return value.
  • TypeScript types: Tighten callback signatures to encourage/require a return type.

Workarounds and best practices for this repository

  • In Express routes, always either:
    • return res.json(...) (or return next(...)) from inside the startActiveSpan callback; or
    • Return a value/promise that the caller awaits; and
    • Use try/finally to end the span reliably.

Example pattern:

app.get("/example", async (req, res) => {
  const tracer = trace.getTracer("service");
  return tracer.startActiveSpan("handler", async (span) => {
    try {
      span.setAttribute("http.method", req.method);
      // ... your logic ...
      return res.json({ ok: true });
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: 2, message: String(err) });
      throw err;
    } finally {
      span.end();
    }
  });
});

Additional context

  • Issue reproduced with both SimpleSpanProcessor and other processors.
  • Observed primarily in Express-style handlers, but pattern can affect any callback usage.
  • ESM environment, modern Node.js.

Status

This document records the observed behavior and recommended mitigations. If upstream changes or improved types/warnings become available, we should update this file and reference the relevant package versions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpkg:apipkg:sdk-trace-basepriority:p2Bugs and spec inconsistencies which cause telemetry to be incomplete or incorrect

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions