BoundaryML/baml · critical · BridgeError

Engine lock poisoned

Error message

Engine lock poisoned

What it means

BridgeError::LockPoisoned is returned when the global RUNTIME_INSTANCE RwLock (read or write) could not be acquired because another thread panicked while holding the lock, leaving it poisoned (see get_runtime/replace_runtime/take_runtime in lib_native.rs). Once poisoned, every subsequent bridge operation touching the engine slot fails with this error. It signals a prior panic inside engine-protected code rather than a problem with the current call.

Source

Thrown at baml_language/crates/bridge_cffi/src/error.rs:16

//! Error types for bridge_cffi.

use thiserror::Error;

/// Errors that can occur during bridge operations.
#[derive(Debug, Error)]
pub enum BridgeError {
    #[error(transparent)]
    Ctypes(#[from] bridge_ctypes::CtypesError),
    #[error("Engine not initialized. Call create_baml_runtime first.")]
    NotInitialized,

    #[error("Project not initialized")]
    ProjectNotInitialized,

    #[error("Engine lock poisoned")]
    LockPoisoned,

    #[error("{0}")]
    Runtime(#[from] bex_project::RuntimeError),

    #[error("CallFunctionArgs.call_target must be set")]
    MissingCallTarget,

    #[error("type arguments are not supported when invoking a BAML function handle")]
    FunctionHandleTypeArgs,

    #[error("Function not found: {name}")]
    FunctionNotFound { name: String },

    #[error("Missing argument '{parameter}' for function '{function}'")]
    MissingArgument { function: String, parameter: String },

    #[error("Not implemented: {0}")]

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Find and fix the root-cause panic that poisoned the lock (check logs/stderr for the original panic backtrace).
  2. Restart the process or re-initialize the bridge module — poisoning is per-lock and does not self-heal.
  3. In the bridge, switch to parking_lot::RwLock (no poisoning) or recover from poison via lock.unwrap_or_else(|e| e.into_inner()) if the state is still valid.
  4. Avoid panics inside engine callbacks by converting failures to BridgeError results instead of unwrapping.

Example fix

// before
RUNTIME_INSTANCE
    .read()
    .map_err(|_| BridgeError::LockPoisoned)?
    .clone()
    .ok_or(BridgeError::NotInitialized)
// after
RUNTIME_INSTANCE
    .read()
    .unwrap_or_else(|poisoned| poisoned.into_inner())
    .clone()
    .ok_or(BridgeError::NotInitialized)
Defensive patterns

Strategy: try-catch

Try / catch

match get_runtime() {
    Err(BridgeError::LockPoisoned) => {
        // a prior thread panicked holding the engine lock; restart/re-init
        restart_bridge();
    }
    other => other?,
}

Prevention

When it happens

Trigger: A panic occurred in any thread while holding the RUNTIME_INSTANCE read/write guard (e.g. inside an engine callback, shutdown, or install path); afterwards, get_runtime, replace_runtime, or take_runtime call .lock()/.read()/.write() and map the Err poison result to BridgeError::LockPoisoned.

Common situations: A panicked async task or callback earlier in the run corrupted the lock; concurrent hot-reload/shutdown racing with in-flight calls; tests that let one setup thread panic and then observe all later calls fail; bug in an embedded host calling the bridge reentrantly from a panic hook.

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