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.

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]}`)
}