BoundaryML/baml · error

UnscheduledFuture cannot be deserialized

Error message

UnscheduledFuture cannot be deserialized

What it means

Companion to the `UnscheduledFuture` serialization stub: its `BorshDeserialize` impl always returns `Err(InvalidData)` and can never succeed. A wire payload claiming to contain an `UnscheduledFuture` means a malformed program/pack, and the library fails fast instead of reconstructing a runtime spawn slot.

Source

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

}

// `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`
// becomes UB on the second read. Guard against that at compile time.
const _: () = {
    const fn assert_copy<T: Copy>() {}
    assert_copy::<Value>();
};

/// Discriminant byte for [`Future::state`].
#[repr(u8)]
enum FutureTag {

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Regenerate the pack with the trusted compiler; an artifact containing this variant is malformed.
  2. Fix the producer so spawn-request values are excluded from the serialized constant heap.
  3. If persistence is needed, store a registry handle/id and re-create the spawn slot at runtime on load.

Example fix

// before: trusting a pack that embeds spawn slots
let obj: Object = borsh::from_reader(&mut reader)?; // io::Error: UnscheduledFuture cannot be deserialized
// after: reject such artifacts up front / fix the pack source
let pack = PackEnvelope::parse(&bytes)?; // validates the object pool excludes runtime-only variants
Defensive patterns

Strategy: validation

Validate before calling

// validate pack contents before deserializing objects
if matches!(proxy, ObjectWire::UnscheduledFuture(_)) {
    return Err(io::Error::new(io::ErrorKind::InvalidData, "malformed pack: unscheduled future"));
}

Type guard

fn is_unscheduled_future_wire(o: &ObjectWire) -> bool {
    matches!(o, ObjectWire::UnscheduledFuture(_))
}

Prevention

When it happens

Trigger: Calling `UnscheduledFuture::deserialize_reader` / `borsh::from_reader::<UnscheduledFuture>`, or deserializing an `ObjectWire::UnscheduledFuture` payload during pack loading.

Common situations: Loading a corrupted or hand-edited BAML pack whose object pool encodes an unscheduled-future object; a producer-side bug that serialized runtime spawn state (which itself would have failed first); format fuzzing.

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/cc451469f6e6e96a. Report an issue: GitHub.