pydantic/monty · error · Error

${what} produced no turn-ending event (worker crashed)

Error message

${what} produced no turn-ending event (worker crashed)

What it means

`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.

Source

Thrown at crates/monty-js/ts/worker/transport.ts:313

        const aborted = await this.run({ tag: 'abort-feed', val: { excType: 'RuntimeError', message } }, onPrint)
        turn = aborted ? this.toTurn(aborted) : crashed('worker exited without a turn-ending event')
        // the component answers an abort with an error, never a suspension;
        // servicing one would let a compromised worker call the host past
        // the budget, so it ends the worker instead
        if (turn.kind !== 'error' && turn.kind !== 'crashed') {
          this.dead = true
          turn = { kind: 'protocol', message: `worker answered abort-feed with ${turn.kind}` }
        }
      }
    }
    if (turn.kind === 'crashed') this.dead = true
    return turn
  }

  /** Sends a control request and verifies its expected event kind. */
  private async control(request: ComponentRequest, kind: ComponentEvent['tag'], what: string): Promise<ComponentEvent> {
    const event = await this.run(request, undefined)
    if (!event) throw new Error(`${what} produced no turn-ending event (worker crashed)`)
    if (event.tag !== kind) throw new Error(`${what} expected event ${kind}, got ${event.tag}`)
    return event
  }

  /** Runs one turn, forwarding buffered prints and retaining its terminator. */
  private async run(request: ComponentRequest, onPrint: OnPrint | undefined): Promise<ComponentEvent | null> {
    let events: ComponentEvent[]
    try {
      const result = await this.dispatcher(request)
      if (result.status === 'shutdown') this.dead = true
      // the component reports the limit in force (the configured one, else
      // the 1000 default; a dump's on load), so it is adopted from the reply
      if (request.tag === 'configure' || request.tag === 'load') {
        this.suspensionLimit = result.maxSuspensions
        this.suspensionsSeen = 0n
      }
      events = result.events
    } catch {

View on GitHub (pinned to adc986b362)

Solutions

  1. Discard the session and create a fresh one (pool.checkout()); the crashed worker cannot answer further requests.
  2. Inspect what the session was executing for OOM/timeout triggers (huge allocations, runaway loops) — hard limits intentionally end the worker.
  3. Check browser console / Node stderr for the underlying wasm trap message to distinguish memory traps from component bugs.
  4. If crashes are reproducible with trivial code, rebuild the component (`make build-wasm`) and re-test to rule out a stale/incorrect build.

Example fix

// before
const bytes = await session.dump() // worker crashed -> throws

// after
try {
  const bytes = await session.dump()
} catch (err) {
  // worker crashed; replace the session
  session = await pool.checkout()
  await session.feedRun(setupCode)
}
Defensive patterns

Strategy: retry

Try / catch

try {
  result = await session.dump()
} catch (err) {
  if (err instanceof Error && err.message.includes('produced no turn-ending event (worker crashed)')) {
    session = await pool.checkout() // replace crashed worker's session
    result = await session.dump()
  } else throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/53f149181aed7ebf. Report an issue: GitHub.