swc-project/swc · error · anyhow::Error

failed to restore source context because the source length c

Error message

failed to restore source context because the source length changed while passing AST between JavaScript and native code

What it means

The Node binding (@swc/core native layer) can accept an already-parsed program instead of raw source: JS sends a ProgramEnvelope { program, sourceContext } and bindings/binding_core_node rebuilds a SourceFile from sourceContext.source, then verifies the fresh file's byte length equals the recorded range (endPos - startPos) before rebasing every AST span into the new source map (prepare_program_with_context). A mismatch triggers this bail! — SWC spans are raw byte offsets into the global source map, so a length drift would silently corrupt all spans and sourcemaps, and the check refuses to proceed.

Source

Thrown at bindings/binding_core_node/src/ast_context.rs:88

}

pub fn prepare_program_with_context(
    c: &Compiler,
    mut program: Program,
    source_context: ProgramSourceContext,
) -> Result<(swc_core::common::sync::Lrc<SourceFile>, Program), Error> {
    let fm = c.cm.new_source_file(
        source_context.file_name().into(),
        source_context.source.clone(),
    );

    let expected_len = source_context
        .end_pos
        .checked_sub(source_context.start_pos)
        .context("invalid source context byte range")?;

    if fm.byte_length() != expected_len {
        bail!(
            "failed to restore source context because the source length changed while passing AST \
             between JavaScript and native code"
        );
    }

    rebase_program_spans(&mut program, &source_context, &fm)
        .context("failed to rebase AST spans to restored source context")?;

    Ok((fm, program))
}

fn rebase_program_spans(
    program: &mut Program,
    source_context: &ProgramSourceContext,
    fm: &SourceFile,
) -> Result<(), Error> {
    let old_start = source_context.start_pos;
    let old_end = source_context.end_pos;

View on GitHub (pinned to 5176682b65)

Solutions

  1. Pass the byte-exact same source string that produced the AST's spans (freeze it in a const immediately after parsing).
  2. Re-parse the modified source instead of reusing the stale AST — fresh spans always match the new text.
  3. Normalize line endings/BOM once, before parsing, and feed the identical normalized string to every subsequent call.
  4. Align @swc/core JS and native package versions so envelope handling matches.

Example fix

// before: source mutated between parse and native call
const ast = await parse(src);
src = src.replaceAll("\r\n", "\n"); // length changed -> span corruption
await print(ast, { sourceContext });

// after: normalize first, then parse, then reuse both unchanged
src = src.replaceAll("\r\n", "\n");
const ast = await parse(src);
await print(ast, { sourceContext });
Defensive patterns

Strategy: try-catch

Validate before calling

import { Buffer } from "node:buffer";
// run before any @swc/core call that receives a parsed program envelope
export function sourceContextIsConsistent({ source, startPos, endPos }) {
  const start = Number(startPos ?? 0);
  const end = Number(endPos ?? 0);
  if (!Number.isInteger(start) || !Number.isInteger(end) || end < start) return false;
  return Buffer.byteLength(source, "utf8") === end - start;
}
if (!sourceContextIsConsistent(envelope.sourceContext)) {
  throw new Error("source drifted from AST spans — re-parse before calling native");
}

Type guard

/**
 * Narrows a ProgramSourceContext to one whose byte length matches its span range,
 * i.e. safe to pass to the native restore path.
 */
function isRestorableSourceContext(c) {
  return Buffer.byteLength(c.source, "utf8") === c.endPos - c.startPos;
}

Try / catch

try {
  result = await callNativeWithProgram(program, sourceContext);
} catch (e) {
  if (/failed to restore source context/.test(String(e?.message ?? e))) {
    const reparsed = await parse(sourceContext.source); // regenerate spans from current text
    result = await callNativeWithProgram(reparsed.program, sourceContext);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling a @swc/core Node API that receives a parsed program envelope where the source string differs in byte length from the one the AST's spans were produced against — the code was edited, re-encoded (CRLF->LF, BOM added/removed), or replaced with another file's text between the parse and the call.

Common situations: Mutating the code string after parse() and reusing the old AST; normalizing line endings or BOM between parse and print; mixing spans from the original source with a minified/preprocessed string; version skew between the JS layer and the native binding altering envelope semantics.

Related errors


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/428bf552b2d50b94. Report an issue: GitHub.