BoundaryML/baml · error · napi::Error

Already used span

Error message

Already used span

What it means

Thrown by the N-API Rust binding for a BAML runtime span when finish() is called on a Span that has already been finished or consumed. The span's inner runtime call handle is taken with Option::take(), so a second finish() finds None and raises this error. It guards against double-completing a span, which would corrupt runtime tracing.

Solutions

  1. Ensure finish() is called at most once per span (use a boolean flag or wrap in a once-guard)
  2. Check whether an earlier code path (catch/finally) already finished the span before calling finish again
  3. If you need multiple completions, create a new span instead of reusing the finished one
  4. Catch this napi Error and treat it as an idempotent no-op if double-finish is expected in your flow

Example fix

// before
span.finish(result, ctx, env);
span.finish(result, ctx, env); // throws 'Already used span'
// after
let finished = false;
function finishOnce() {
  if (!finished) {
    finished = true;
    span.finish(result, ctx, env);
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (span.__finished) { return; }

Type guard

function isFinishable(span) { return span && typeof span.finish === 'function' && !span.__finished; }

Try / catch

try { span.finish(result, ctx, envVars); } catch (e) {
  if (String(e?.message).includes('Already used span')) { /* idempotent no-op */ }
  else throw e;
}

Prevention

When it happens

Trigger: Calling span.finish(result, ctx, envVars) twice on the same span object; reusing a span returned by an earlier finish; wrapping finish() in retry logic that re-invokes it after a prior successful call.

Common situations: Instrumentation code that finishes spans in multiple code paths (success and error handlers) without tracking completion; promise/callback flows where a span is finished both in a then and a catch; wrapping frameworks that call finish on teardown after the app already finished the span.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12). Data as JSON: /api/errors/20b6fa5ddce8601b. Report an issue: GitHub.

Appendix: source

Thrown at engine/language_client_typescript/src/types/span.rs:60

        })
    }

    // mthod to finish
    #[napi]
    pub fn finish(
        &mut self,
        result: serde_json::Value,
        ctx: &RuntimeContextManager,
        env_vars: serde_json::Value,
    ) -> napi::Result<serde_json::Value> {
        log::trace!("Finishing span: {:?}", self.inner);
        let result: BamlValue = serde_json::from_value(result)
            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{e:?}")))?;

        let call = self
            .inner
            .take()
            .ok_or_else(|| napi::Error::new(napi::Status::GenericFailure, "Already used span"))?;

        let env_vars: HashMap<String, String> = serde_json::from_value(env_vars)
            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{e:?}")))?;

        self.rt
            .finish_call(call, Some(result), &ctx.inner, &env_vars)
            .map(|u| u.to_string())
            .map(|u| serde_json::json!(u))
            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{e:?}")))
    }
}

View on GitHub (pinned to bd85ce9dee)