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

  1. Check host logs immediately preceding this panic for the worker's termination reason and address it (fix the module code, raise memory limits).
  2. Keep procedures short and free of unbounded loops/allocations so they complete within worker lifetime.
  3. Republish with matching JS SDK and server versions.
  4. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20). Data as JSON: /api/errors/51f383d89a072b16. Report an issue: GitHub.