Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

spawn

Signature: spawn<T>(fn: () => T): Promise<T> — imported from perry/thread.

Runs a closure on a new OS thread and returns a Promise that resolves when the thread completes. The main thread continues immediately — UI and other work are not blocked.

Basic Usage

async function spawnBasic(): Promise<void> {
    const result = await spawn(() => {
        // This runs on a separate OS thread.
        let sum = 0
        for (let i = 0; i < 100_000_000; i++) {
            sum += i
        }
        return sum
    })

    console.log(result) // 4999999950000000
}

Non-Blocking

spawn returns immediately. The main thread doesn’t wait:

async function spawnNonBlocking(): Promise<void> {
    console.log("1. Starting background work")

    const handle = spawn(() => {
        // Runs on a background thread — heavier work elided here.
        let n = 0
        for (let i = 0; i < 10_000_000; i++) n++
        return n
    })

    console.log("2. Main thread continues immediately")

    const result = await handle
    console.log(`3. Got result: ${result}`)
}

Output:

1. Starting background work
2. Main thread continues immediately
3. Got result: <computed value>

Multiple Concurrent Tasks

Spawn multiple tasks and they run truly concurrently — one OS thread per spawn call:

async function spawnMultiple(): Promise<void> {
    const t1 = spawn(() => analyseChunk(0, 1_000_000))
    const t2 = spawn(() => analyseChunk(1_000_000, 2_000_000))
    const t3 = spawn(() => analyseChunk(2_000_000, 3_000_000))

    // All three run simultaneously on separate OS threads.
    const results = await Promise.all([t1, t2, t3])

    console.log(`Region A: ${results[0]}`)
    console.log(`Region B: ${results[1]}`)
    console.log(`Region C: ${results[2]}`)
}

function analyseChunk(start: number, end: number): number {
    let acc = 0
    for (let i = start; i < end; i++) acc += i & 0xff
    return acc
}

Unlike Node.js worker_threads, each spawn is a lightweight OS thread (~8MB stack), not a full V8 isolate (~2MB heap + startup cost).

Capturing Variables

Like parallelMap, spawn closures can capture outer variables. They are deep-copied to the background thread:

async function spawnCapture(): Promise<void> {
    const config = { iterations: 1000, seed: 42 }
    const dataset = [1, 2, 3, 4, 5, 6, 7, 8]

    const result = await spawn(() => {
        // config and dataset are deep-copied to this thread.
        let acc = config.seed
        for (let i = 0; i < config.iterations; i++) {
            acc = (acc * 1103515245 + 12345) & 0x7fffffff
        }
        for (const v of dataset) acc ^= v
        return acc
    })

    console.log(`spawn-capture: ${result}`)
}

Mutable variables cannot be captured — this is enforced at compile time.

File System Handles

Do not capture numeric fds or fs.promises.FileHandle objects for file I/O in spawn. Perry’s fd registry is per thread: a numeric fd captured from the main thread is not open in the worker, and a captured FileHandle arrives detached with fd === -1. Capture a path string instead, then call fs.openSync or fs.promises.open inside the spawned function.

Returning Complex Values

spawn can return any value type. Complex values (objects, arrays, strings) are serialized back to the main thread automatically:

async function spawnComplexReturn(): Promise<void> {
    const stats = await spawn(() => {
        const values = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]
        let sum = 0
        let max = values[0]
        let min = values[0]
        for (const v of values) {
            sum += v
            if (v > max) max = v
            if (v < min) min = v
        }
        return {
            mean: sum / values.length,
            min,
            max,
            count: values.length,
        }
    })

    console.log(`mean=${stats.mean} min=${stats.min} max=${stats.max} count=${stats.count}`)
}

UI Integration

spawn is ideal for keeping native UIs responsive during heavy computation:

const analyzeButton = Button("Analyze", async () => {
    status.set("Processing...")

    // Background thread — UI stays responsive
    const data = await spawn(() => {
        let count = 0
        for (let i = 0; i < 1_000_000; i++) {
            if ((i & 0xff) === 0) count++
        }
        return { count }
    })

    result.set(`Found ${data.count} patterns`)
    status.set("Done")
})

Without spawn, the analysis would freeze the UI. With spawn, the user can still scroll, tap other buttons, or navigate while the computation runs.

Compared to Node.js worker_threads

// ── Node.js: ~15 lines, separate file needed ──────────
// worker.js
const { parentPort, workerData } = require("worker_threads");
const result = heavyComputation(workerData);
parentPort.postMessage(result);

// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js", {
    workerData: inputData,
});
worker.on("message", (result) => {
    console.log(result);
});
worker.on("error", (err) => { /* handle */ });


// ── Perry: 1 line ─────────────────────────────────────
// const result = await spawn(() => heavyComputation(inputData));

No separate files. No message ports. No event handlers. No structured clone. One line.

Examples

Background File Processing

async function spawnBgFile(): Promise<void> {
    // Read and process a "large" file without blocking. We inline a tiny CSV
    // so the snippet runs hermetically — the docs' real version would call
    // readFileSync from "fs".
    const content = "id,value\n1,10\n2,20\n3,30\n"
    const analysis = await spawn(() => {
        const lines = content.split("\n").filter((l: string) => l.length > 0).slice(1)
        let total = 0
        for (const line of lines) {
            const parts = line.split(",")
            total += parseInt(parts[1], 10)
        }
        return { rows: lines.length, total }
    })

    console.log(`spawn-bg-file rows=${analysis.rows} total=${analysis.total}`)
}

Parallel API Calls with Processing

async function spawnApiThenProcess(): Promise<void> {
    // The docs example fetches a remote API; for a hermetic test we
    // just hand-roll the same pipeline shape with synthetic data.
    const rawData = { items: [1, 2, 3, 4, 5] }

    // CPU-intensive processing happens off the main thread
    const processed = await spawn(() => {
        let total = 0
        for (const v of rawData.items) total += v * v
        return { total, count: rawData.items.length }
    })

    console.log(`spawn-api-then-process total=${processed.total} count=${processed.count}`)
}

Deferred Computation

async function spawnDeferred(): Promise<void> {
    const params = { size: 8 }

    // Start computation early, use result later
    const precomputed = spawn(() => {
        const table: number[] = []
        for (let i = 0; i < params.size; i++) table.push(i * i)
        return table
    })

    // ... do other setup work ...

    // Result is ready (or we wait for it)
    const table = await precomputed
    console.log(`spawn-deferred len=${table.length} last=${table[table.length - 1]}`)
}