BoundaryML/baml · error

HostClosure cannot be serialized

Error message

HostClosure cannot be serialized

What it means

`Object::HostClosure` is a closure backed by a host (Rust) callable that cannot be represented in the Borsh wire format, so `Object`'s `BorshSerialize` fails fast with `InvalidData` when it reaches that variant. Host closures are runtime-only values and must never end up in a serialized pack or heap snapshot.

Source

Thrown at baml_language/crates/bex_vm_types/src/types/object.rs:263

                v.key_ty.clone(),
                v.value_ty.clone(),
                v.to_index_map()
                    .into_iter()
                    .map(|(k, v)| (k.to_string(), v))
                    .collect(),
            ),
            Self::Float(v) => ObjectWire::Float(*v),
            Self::Future(v) => ObjectWire::Future(v.clone()),
            Self::UnscheduledFuture(v) => ObjectWire::UnscheduledFuture(v.clone()),
            Self::Type(v) => ObjectWire::Type(Box::new(v.ty.clone())),
            Self::RustData(_) => {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "RustData cannot be serialized",
                ));
            }
            Self::HostClosure(_) => {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "HostClosure cannot be serialized",
                ));
            }
            #[cfg(feature = "heap_debug")]
            Self::Sentinel(_) => {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "Sentinel cannot be serialized",
                ));
            }
        };
        proxy.serialize(writer)
    }
}

impl BorshDeserialize for Object {
    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Exclude host closures from the serialized value graph (drop or unregister them before exporting/packing).
  2. Store host callables in a host-side registry and reference them from the VM by id (an `Object` variant that serializes as an integer).
  3. Audit pack-export input: only serialize the compiled program's object pool, never the live runtime heap.

Example fix

// before: exporting a heap where a map still holds a host callback
let pack = PackEnvelope::new(program, heap_containing_host_closure); // io::Error: HostClosure cannot be serialized
// after: keep callbacks in a registry, reference by id
let id = host_registry.insert(my_callback);
heap.store_global("on_event", Value::Int(id as i64));
let pack = PackEnvelope::new(program, heap);
Defensive patterns

Strategy: validation

Validate before calling

// before pack export, scan for host callables
fn has_host_closure(v: &Value) -> bool {
    matches!(v, Value::Object(o) if matches!(&*o.borrow(), Object::HostClosure(_)))
}

Type guard

fn is_host_closure(o: &Object) -> bool {
    matches!(o, Object::HostClosure(_))
}

Prevention

When it happens

Trigger: Serializing an `Object` (directly, or via `Value`/object-pool/pack export) holding `Self::HostClosure(_)` — typically a host-registered callback captured by VM code or stored in a reachable location at export time.

Common situations: Registering a Rust callback (e.g. for `f.cancel()` hooks or FFI bridges) that a BAML program stores in a global/map/array which is then included in pack serialization; heap-snapshot tooling run against a live session; accidental capture of host functions in compiled constants.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


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