{"record":{"id":"caddec469c7f8e3d","repo":"clockworklabs/SpacetimeDB","slug":"js-worker-exited-before-accepting-ctx","errorCode":null,"errorMessage":"JS worker exited before accepting `{ctx}`","messagePattern":"JS worker exited before accepting `(.+?)`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/core/src/host/v8/mod.rs","lineNumber":745,"sourceCode":"    ) -> CallScheduledFunctionResult {\n        self.send_request(\"scheduled_procedure\", |reply_tx| {\n            JsProcedureWorkerRequest::ScheduledProcedure { reply_tx, params }\n        })\n        .await\n    }\n}\n\nasync fn send_js_request<Req, T>(\n    ctx: &'static str,\n    tx: &mpsc::Sender<Req>,\n    request: impl FnOnce(JsReplyTx<T>) -> Req,\n) -> T\nwhere\n    Req: Send + 'static,\n{\n    let (reply_tx, reply_rx) = oneshot::channel();\n    if tx.send(request(reply_tx)).await.is_err() {\n        panic!(\"JS worker exited before accepting `{ctx}`\");\n    }\n    match reply_rx.await {\n        Ok(Ok(value)) => value,\n        Ok(Err(panic)) => panic::resume_unwind(panic),\n        Err(_) => panic!(\"JS worker exited before replying to `{ctx}`\"),\n    }\n}\n\nasync fn send_js_unbounded_request<T>(\n    ctx: &'static str,\n    tx: &MeteredUnboundedSender<JsMainWorkerRequest>,\n    request: impl FnOnce(JsReplyTx<T>) -> JsMainWorkerRequest,\n) -> T {\n    let (reply_tx, reply_rx) = oneshot::channel();\n    if tx.send(request(reply_tx)).is_err() {\n        panic!(\"JS worker exited before accepting `{ctx}`\");\n    }\n    match reply_rx.await {","sourceCodeStart":727,"sourceCodeEnd":763,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/9e0d92412ff2248f401a8ad12d535f2b5ac30912/crates/core/src/host/v8/mod.rs#L727-L763","documentation":"Host-side panic in SpacetimeDB's V8 host (crates/core): a bounded mpsc send of a JS request failed because the receiving JS worker thread has already exited and dropped its channel. The `send_js_request` helper is used for procedure/scheduled-procedure calls into the JavaScript quickjs/v8 runtime; if the worker is dead before accepting, the host cannot proceed and panics with the request context (e.g. `call_procedure`, `scheduled_procedure`). It almost always means the JS worker crashed earlier — its panic is the root cause and appears in logs just before this.","triggerScenarios":"A JS module procedure panics or calls process.exit-style fatal paths, killing the worker thread; subsequent `call_procedure`/`call_http_handler`/`scheduled_procedure` sends then hit the closed channel. Also possible during host shutdown races where requests are still being routed to a terminated worker.","commonSituations":"JavaScript quickstart modules throwing uncaught exceptions in reducers/procedures; OOM or V8 fatal errors (stack overflow, invalid wasm); SDK version mismatch between the JS module and the host runtime.","solutions":["Look earlier in the host logs for the worker's original crash (uncaught JS exception, OOM, V8 fatal) — fix that first; the context string names which request failed.","Rebuild and republish the JS module with an SDK version matching the server.","Wrap risky JS entry points in try/catch so module errors return errors instead of killing the worker.","If the crash happens during shutdown/deploy races, update the spacetimedb server — worker lifetime bugs around host shutdown are fixed over time."],"exampleFix":"// JS module: before - uncaught throw kills the worker\nexport function transfer(ctx, from, to, amt) {\n  const acct = db.account.findByOwner(from);\n  acct.balance -= amt; // throws if acct undefined\n}\n\n// after\nexport function transfer(ctx, from, to, amt) {\n  const acct = db.account.findByOwner(from);\n  if (!acct || acct.balance < amt) throw new Error(\"insufficient funds\"); // handled by host, worker survives\n  acct.balance -= amt;\n  db.account.update(acct);\n}","handlingStrategy":"validation","validationCode":"// Host-embedded pattern: the handle exposes is_closed() (tx.is_closed()).\n// Guard before sending so a dead worker is handled, not fatal:\nif js_instance.is_closed() {\n    return Err(HostError::js_worker_unavailable(\"call_procedure\"));\n}\nlet ret = js_instance.call_procedure(params).await;","typeGuard":null,"tryCatchPattern":"// In host code: catch the panic at the call boundary and translate to an error.\nlet outcome = tokio::task::spawn(async move { js_instance.call_procedure(params).await }).await;\nmatch outcome {\n    Ok(ret) => Ok(ret),\n    Err(join_err) if join_err.is_panic() => Err(HostError::js_worker_panicked(join_err.into_panic())),\n    Err(_) => Err(HostError::js_worker_died),\n}","preventionTips":["Check `is_closed()` on the JS instance before issuing requests when teardown is possible.","Wrap JS entry points in try/catch so module bugs return errors instead of killing the worker.","Keep JS module SDK versions aligned with the host; test procedures under load before deploy."],"tags":["spacetimedb","javascript","v8","worker-crash","host-panic","channel-closed"],"backgroundTag":"worker-thread-crashed","analyzedSha":"9e0d92412ff2248f401a8ad12d535f2b5ac30912","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}