{"record":{"id":"53f149181aed7ebf","repo":"pydantic/monty","slug":"what-produced-no-turn-ending-event-worker-crashed","errorCode":null,"errorMessage":"${what} produced no turn-ending event (worker crashed)","messagePattern":"(.+?) produced no turn-ending event \\(worker crashed\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/worker/transport.ts","lineNumber":313,"sourceCode":"        const aborted = await this.run({ tag: 'abort-feed', val: { excType: 'RuntimeError', message } }, onPrint)\n        turn = aborted ? this.toTurn(aborted) : crashed('worker exited without a turn-ending event')\n        // the component answers an abort with an error, never a suspension;\n        // servicing one would let a compromised worker call the host past\n        // the budget, so it ends the worker instead\n        if (turn.kind !== 'error' && turn.kind !== 'crashed') {\n          this.dead = true\n          turn = { kind: 'protocol', message: `worker answered abort-feed with ${turn.kind}` }\n        }\n      }\n    }\n    if (turn.kind === 'crashed') this.dead = true\n    return turn\n  }\n\n  /** Sends a control request and verifies its expected event kind. */\n  private async control(request: ComponentRequest, kind: ComponentEvent['tag'], what: string): Promise<ComponentEvent> {\n    const event = await this.run(request, undefined)\n    if (!event) throw new Error(`${what} produced no turn-ending event (worker crashed)`)\n    if (event.tag !== kind) throw new Error(`${what} expected event ${kind}, got ${event.tag}`)\n    return event\n  }\n\n  /** Runs one turn, forwarding buffered prints and retaining its terminator. */\n  private async run(request: ComponentRequest, onPrint: OnPrint | undefined): Promise<ComponentEvent | null> {\n    let events: ComponentEvent[]\n    try {\n      const result = await this.dispatcher(request)\n      if (result.status === 'shutdown') this.dead = true\n      // the component reports the limit in force (the configured one, else\n      // the 1000 default; a dump's on load), so it is adopted from the reply\n      if (request.tag === 'configure' || request.tag === 'load') {\n        this.suspensionLimit = result.maxSuspensions\n        this.suspensionsSeen = 0n\n      }\n      events = result.events\n    } catch {","sourceCodeStart":295,"sourceCodeEnd":331,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/worker/transport.ts#L295-L331","documentation":"`control()` sends a control request (create, dump, finish, etc.) to the wasm worker and treats 'no turn-ending event' as a hard failure: `run()` returned null, which only happens when the worker exited or crashed without producing an event. The error names the operation (`what`) so you can tell which control step lost its worker.","triggerScenarios":"The worker process/component died between sending the control request and receiving its event — e.g. wasm trap (OOM hard limit), browser `Worker.terminate()`, or the in-process degrade throwing during execution — so `run()` resolves null.","commonSituations":"Python code in the session triggering a hard memory-limit trap; the host page terminating workers (navigation, watchdog); a component bug that exits the worker mid-request; resource exhaustion killing the worker before it can answer.","solutions":["Discard the session and create a fresh one (pool.checkout()); the crashed worker cannot answer further requests.","Inspect what the session was executing for OOM/timeout triggers (huge allocations, runaway loops) — hard limits intentionally end the worker.","Check browser console / Node stderr for the underlying wasm trap message to distinguish memory traps from component bugs.","If crashes are reproducible with trivial code, rebuild the component (`make build-wasm`) and re-test to rule out a stale/incorrect build."],"exampleFix":"// before\nconst bytes = await session.dump() // worker crashed -> throws\n\n// after\ntry {\n  const bytes = await session.dump()\n} catch (err) {\n  // worker crashed; replace the session\n  session = await pool.checkout()\n  await session.feedRun(setupCode)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  result = await session.dump()\n} catch (err) {\n  if (err instanceof Error && err.message.includes('produced no turn-ending event (worker crashed)')) {\n    session = await pool.checkout() // replace crashed worker's session\n    result = await session.dump()\n  } else throw err\n}","preventionTips":["Bound sandbox memory/loops so hard-limit traps are rare.","Listen for worker error/exit events to detect crashes early.","Keep one retry-and-replace path around all turn-level calls.","Inspect wasm trap logs to distinguish OOM from component bugs."],"tags":["wasm","crash","wasm-worker","worker"],"backgroundTag":"unexpected-response-shape","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}