clockworklabs/SpacetimeDB · critical
JS worker exited before replying to `{ctx}`
Error message
JS worker exited before replying to `{ctx}` What it means
Host-side panic in SpacetimeDB's V8 host: a JS request was accepted by the worker (the send succeeded) but the reply channel was dropped without a reply — the oneshot receiver returned Err. This means the JS worker exited after taking the request but before answering, i.e. it crashed or was torn down mid-request. Any panic payload the worker did propagate arrives via the Ok(Err(panic)) arm and is resumed as the original panic; this Err(_) arm is specifically 'worker died silently'.
Source
Thrown at crates/core/src/host/v8/mod.rs:750
}
}
async fn send_js_request<Req, T>(
ctx: &'static str,
tx: &mpsc::Sender<Req>,
request: impl FnOnce(JsReplyTx<T>) -> Req,
) -> T
where
Req: Send + 'static,
{
let (reply_tx, reply_rx) = oneshot::channel();
if tx.send(request(reply_tx)).await.is_err() {
panic!("JS worker exited before accepting `{ctx}`");
}
match reply_rx.await {
Ok(Ok(value)) => value,
Ok(Err(panic)) => panic::resume_unwind(panic),
Err(_) => panic!("JS worker exited before replying to `{ctx}`"),
}
}
async fn send_js_unbounded_request<T>(
ctx: &'static str,
tx: &MeteredUnboundedSender<JsMainWorkerRequest>,
request: impl FnOnce(JsReplyTx<T>) -> JsMainWorkerRequest,
) -> T {
let (reply_tx, reply_rx) = oneshot::channel();
if tx.send(request(reply_tx)).is_err() {
panic!("JS worker exited before accepting `{ctx}`");
}
match reply_rx.await {
Ok(Ok(value)) => value,
Ok(Err(panic)) => panic::resume_unwind(panic),
Err(_) => panic!("JS worker exited before replying to `{ctx}`"),
}
}View on GitHub (pinned to 9e0d92412f)
Solutions
- Check host logs immediately preceding this panic for the worker's termination reason and address it (fix the module code, raise memory limits).
- Keep procedures short and free of unbounded loops/allocations so they complete within worker lifetime.
- Republish with matching JS SDK and server versions.
- Avoid issuing calls during redeploy windows; if the race recurs on current versions, report it to SpacetimeDB with the request context string.
Defensive patterns
Strategy: try-catch
Validate before calling
// Before awaiting a JS request, verify the worker is still alive:
if js_instance.is_closed() {
return Err(HostError::js_worker_unavailable(ctx_name));
}
// After send succeeds, wrap the await in a task to observe worker death:
let ret = tokio::spawn(js_instance.call_procedure(params)).await
.map_err(|_| HostError::js_worker_died_mid_request(ctx_name))?; Try / catch
match reply_rx.await {
Ok(Ok(value)) => Ok(value),
Ok(Err(panic)) => { log::error!("JS procedure panicked: {panic:?}"); Err(HostError::js_module_panic) }
Err(_) => { log::error!("JS worker died mid-request `{ctx}`"); Err(HostError::js_worker_died) }
} Prevention
- Bound memory and iteration in JS procedures so workers cannot be OOM-killed mid-call.
- Log and fix the first worker crash — this panic is always a downstream symptom.
- Avoid issuing JS calls during module redeploy or host shutdown windows.
When it happens
Trigger: The JS worker aborting while executing the request: uncaught exception paths that skip the reply, V8 fatal errors (OOM, stack overflow), or the host tearing down the worker during deploy/shutdown while a procedure call is in flight.
Common situations: Long-running JS procedures that exhaust memory; redeploying a module while scheduled procedures execute; JS SDK/runtime mismatches causing the worker to bail.
Related errors
- JS worker exited before accepting `{ctx}`
- default export is not a Schema object
- must export schema as default export
- cannot serialize refs without a typespace
- cannot deserialize refs without a typespace
AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20).
Data as JSON: /api/errors/51f383d89a072b16.
Report an issue: GitHub.