BoundaryML/baml · error

UnscheduledFuture cannot be serialized

Error message

UnscheduledFuture cannot be serialized

What it means

`UnscheduledFuture` is a runtime spawn-request slot with the same lifecycle shape as `Future` and is never baked into a compiled `Program`. Its `BorshSerialize` impl is an intentional always-fail stub: if an `UnscheduledFuture` reaches the serializer (the `baml_exec::PackEnvelope` that serializes bytecode + constant heap), the program is malformed and the library fails fast with `InvalidData`.

Source

Thrown at baml_language/crates/bex_vm_types/src/types/future.rs:144

}

impl BorshDeserialize for Future {
    fn deserialize_reader<R: std::io::Read>(_reader: &mut R) -> std::io::Result<Self> {
        Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "Future cannot be deserialized",
        ))
    }
}

// `UnscheduledFuture` is a runtime spawn-request slot — same lifecycle
// shape as `Future`, never appears in a compiled `Program`. The pack
// envelope (`baml_exec::PackEnvelope`) serializes the bytecode + the
// constant heap; if an `UnscheduledFuture` ever reaches the serializer
// that's a malformed program and we want to fail fast.
impl BorshSerialize for UnscheduledFuture {
    fn serialize<W: std::io::Write>(&self, _writer: &mut W) -> std::io::Result<()> {
        Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "UnscheduledFuture cannot be serialized",
        ))
    }
}

impl BorshDeserialize for UnscheduledFuture {
    fn deserialize_reader<R: std::io::Read>(_reader: &mut R) -> std::io::Result<Self> {
        Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "UnscheduledFuture cannot be deserialized",
        ))
    }
}

// `Future::read` calls `MaybeUninit::<Value>::assume_init_read`, which is
// sound only because `Value: Copy`. If `Value` ever gains a non-trivial
// `Drop` (e.g. by holding an `Arc<…>` or `Box<…>`), `assume_init_read`

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Fix the pack builder so spawn-request values never enter the serialized bytecode or constant heap.
  2. Audit the value that reached serialization (`Object::UnscheduledFuture` variant) and route it through a runtime-only path instead.
  3. Rebuild the artifact with a correct compiler; never persist unscheduled spawn slots.

Example fix

// before: packing a live heap that still holds an UnscheduledFuture
let pack = PackEnvelope::new(program, heap_with_spawn_slot); // io::Error: UnscheduledFuture cannot be serialized
// after: strip runtime-only values before packing
let heap = heap_without_runtime_values(&live_heap); // keeps futures/spawn slots out
let pack = PackEnvelope::new(program, heap);
Defensive patterns

Strategy: validation

Validate before calling

// before building a pack
assert!(
    !pool.iter().any(|o| matches!(o, Object::UnscheduledFuture(_))),
    "UnscheduledFuture reached the pack serializer: malformed program"
);

Type guard

fn is_unscheduled_future(v: &Value) -> bool {
    matches!(v, Value::Object(o) if matches!(&*o.borrow(), Object::UnscheduledFuture(_)))
}

Prevention

When it happens

Trigger: Calling `borsh::to_writer`/`serialize` on an `UnscheduledFuture`, or serializing a container (`Object`, `Value`, object pool) that holds one while building a pack.

Common situations: A VM bug that leaks a pending spawn request into the constant heap or object pool before pack serialization; misuse of the public serialization API on live VM values; fuzz/round-trip testing of the pack format.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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