pydantic/monty · error · Error
pool is closed
Error message
pool is closed
What it means
`WorkerPool.checkout()` refuses to borrow a worker once the pool has been closed, because closing shuts down worker dispatch and no new session can be served. This is a fail-fast guard so you get a clear synchronous error rather than hangs or crashes inside `WorkerTransport.create`.
Source
Thrown at crates/monty-js/ts/worker/pool.ts:112
private constructor(
private readonly factory: WorkerFactory,
private readonly maxWorkers: number,
private readonly maxCheckouts: number | undefined,
) {}
/** Creates the pool and prewarms `minWorkers` idle workers. */
static async create(factory: WorkerFactory, options: WorkerPoolOptions = {}): Promise<WorkerPool> {
const max = Math.max(1, options.maxWorkers ?? 4)
const min = Math.min(Math.max(0, options.minWorkers ?? 1), max)
const pool = new WorkerPool(factory, max, options.maxCheckoutsPerWorker)
const warm = await Promise.all(Array.from({ length: min }, () => pool.spawn()))
pool.idle.push(...warm)
return pool
}
/** Borrows a worker and returns a session bound to it. */
async checkout(config: WorkerSessionConfig = {}): Promise<MontySession> {
if (this.closed) throw new Error('pool is closed')
const slot = await this.acquire()
let transport: WorkerTransport
try {
transport = await WorkerTransport.create(slot.worker.dispatch, config)
} catch (err) {
this.discard(slot)
throw err
}
transport.onFinish = (reusable) => this.release(slot, reusable)
return new MontySession(transport as unknown as SessionNative)
}
/** Terminates every worker and rejects anyone still waiting. */
async close(): Promise<void> {
this.closed = true
for (const waiter of this.waiters.splice(0)) waiter.reject(new Error('pool is closed'))
for (const slot of this.idle.splice(0)) {
slot.worker.terminate()View on GitHub (pinned to adc986b362)
Solutions
- Create a fresh pool with `createWorkerPool(modules)` (or `Monty.create()`) and call `checkout()` on that instead of the closed one.
- Move `checkout()` calls inside the lifetime of the pool (`await using pool = ...; ... checkout within that scope`).
- Track pool lifecycle in application code: set a flag or null out the reference when `close()` is awaited so late callers can be rerouted to a new pool.
- In long-running services, keep one pool per application lifetime and never close it per-request.
Example fix
// before await using pool = await createWorkerPool(modules) await pool.close() const session = await pool.checkout() // throws // after await using pool = await createWorkerPool(modules) const session = await pool.checkout() await session.feedRun(code) // pool closes automatically at scope end
Defensive patterns
Strategy: validation
Validate before calling
function ensureOpen(pool: { closed: boolean }) {
if (pool.closed) throw new Error('attempt to checkout a closed pool')
} Type guard
function isUsable(pool: WorkerPool | null | undefined): pool is WorkerPool {
return pool != null && !pool.closed
} Try / catch
try {
session = await pool.checkout(config)
} catch (err) {
if (err instanceof Error && err.message === 'pool is closed') {
pool = await createWorkerPool(modules)
session = await pool.checkout(config)
} else throw err
} Prevention
- Use `await using` so pool lifetime scopes all checkouts.
- Never store the pool in globals that outlive close().
- Null out the pool reference after close() resolves.
- Create one long-lived pool per app instead of closing per operation.
When it happens
Trigger: Calling `pool.checkout(config)` after `await pool.close()` (or after `await using` disposal of the pool) has completed.
Common situations: Reusing a pool object captured in a longer-lived variable after an `await using` scope ends; closing a pool in a shutdown handler while background tasks still hold a reference; accidentally closing in a test fixture teardown that runs before remaining assertions.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- the pool is closed — create a new Monty pool
- loadSession / loadSnapshot is only valid on a fresh session,
- the session is closed — check out a new one
- Monty.create could not auto-load the monty wasm module in th
- nodeWorkerEntry must run as a worker thread
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/58c889386d0c4096.
Report an issue: GitHub.