Database::ensure_schema already produces a proper, recoverable error when the on-disk schema is newer than the process expects. BoxliteRuntime::default_runtime() then converts that error into a panic!, which crosses the Node FFI boundary as a process abort. SDK callers cannot catch it, log it, or degrade — the process dies.
The CLI handles the identical error correctly, which is what makes this look like an oversight rather than a design choice.
The two code paths
src/boxlite/src/db/mod.rs:118 — returns a recoverable error:
Some(v) if v > schema::SCHEMA_VERSION => {
return Err(BoxliteError::Database(format!(
"Schema version mismatch: database has v{}, process expects v{}. \
Upgrade boxlite to a newer version.",
v, schema::SCHEMA_VERSION
)));
}
src/boxlite/src/runtime/core.rs:197 — turns it into a panic:
.unwrap_or_else(|e| panic!("Failed to initialize BoxliteRuntime:\n\n{e}"))
Reproduction
Any version skew across SDKs on one machine reaches this. Verified on Darwin arm64 (macOS 26.1).
# 1. Take the shared database to the current schema with a current SDK
pip install -U boxlite # 0.10.0
python3 -c "
import asyncio, boxlite
async def m():
async with boxlite.SimpleBox(image='alpine:latest') as b:
print(b.id)
asyncio.run(m())"
# 2. Install the newest Node package that can actually load (see #1443)
D=$(mktemp -d) && cd "$D" && npm init -y >/dev/null
npm install @boxlite-ai/boxlite@0.9.7
# 3. Wrap everything in try/catch and run it
cat > t.mjs <<'JS'
import { SimpleBox } from "@boxlite-ai/boxlite";
try {
const box = new SimpleBox({ image: "alpine:latest" });
await box.start();
console.log("started");
} catch (err) {
console.log("CAUGHT:", err.message);
}
console.log("still alive after try/catch");
JS
node t.mjs
Actual:
thread '<unnamed>' (23402646) panicked at src/boxlite/src/runtime/core.rs:173:37:
Failed to initialize BoxliteRuntime:
storage error: Failed to initialize database at /Users/<user>/.boxlite/db/boxlite.db: database error: Schema version mismatch: database has v10, process expects v8. Upgrade boxlite to a newer version.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5, aborting
Neither CAUGHT: nor still alive after try/catch is ever printed. The process aborts inside box.start().
Contrast: the CLI gets this right
Same machine, same database, same underlying error:
$ boxlite ls
Error: storage error: Failed to initialize database at /Users/<user>/.boxlite/db/boxlite.db: database error: Schema version mismatch: database has v10, process expects v8. Upgrade boxlite to a newer version.
Clean message, normal exit, no panic. The error is recoverable — only the SDK entry point discards that.
Impact
Every SDK example in the documentation teaches try/catch (Python) or try/catch/finally (Node) around box creation. That pattern silently does not apply to this failure. A service that creates boxes cannot log the reason, fall back, or return a 500 — the worker process is gone.
The trigger is narrow (version skew), but the failure mode is the worst available: an uncatchable abort in a library.
Suggested fix
Propagate the Result to the binding layer rather than unwrapping it. core.rs:149 already documents a variant that "returns a Result instead of panicking" — the SDK entry points should be using that path, so BoxliteError::Database arrives in Python as an exception and in Node as a rejected promise.
Database::ensure_schemaalready produces a proper, recoverable error when the on-disk schema is newer than the process expects.BoxliteRuntime::default_runtime()then converts that error into apanic!, which crosses the Node FFI boundary as a process abort. SDK callers cannot catch it, log it, or degrade — the process dies.The CLI handles the identical error correctly, which is what makes this look like an oversight rather than a design choice.
The two code paths
src/boxlite/src/db/mod.rs:118— returns a recoverable error:src/boxlite/src/runtime/core.rs:197— turns it into a panic:Reproduction
Any version skew across SDKs on one machine reaches this. Verified on Darwin arm64 (macOS 26.1).
Actual:
Neither
CAUGHT:norstill alive after try/catchis ever printed. The process aborts insidebox.start().Contrast: the CLI gets this right
Same machine, same database, same underlying error:
Clean message, normal exit, no panic. The error is recoverable — only the SDK entry point discards that.
Impact
Every SDK example in the documentation teaches
try/catch(Python) ortry/catch/finally(Node) around box creation. That pattern silently does not apply to this failure. A service that creates boxes cannot log the reason, fall back, or return a 500 — the worker process is gone.The trigger is narrow (version skew), but the failure mode is the worst available: an uncatchable abort in a library.
Suggested fix
Propagate the
Resultto the binding layer rather than unwrapping it.core.rs:149already documents a variant that "returns aResultinstead of panicking" — the SDK entry points should be using that path, soBoxliteError::Databasearrives in Python as an exception and in Node as a rejected promise.