denoland/deno · error · TypeError

Cannot collect output: 'stdout' is locked

Error message

Cannot collect output: 'stdout' is locked

What it means

ChildProcess.output() internally calls collectOutput() on child.stdout, which needs to acquire a reader on the stream. If you already called getReader(), pipeTo(), or pipedThrough() on child.stdout, the stream is locked and output() throws a TypeError before doing anything. You must choose either manual streaming reads or output(), not both.

Source

Thrown at ext/process/40_process.js:535

    };
    signal?.[abortSignal.add](onAbort);
    const waitPromise = op_spawn_wait(this.#rid);
    this.#waitPromise = waitPromise;
    this.#status = PromisePrototypeThen(waitPromise, (res) => {
      signal?.[abortSignal.remove](onAbort);
      this.#waitComplete = true;
      return res;
    });
  }

  #status;
  get status() {
    return this.#status;
  }

  async output() {
    if (this.#stdout?.locked) {
      throw new TypeError(
        "Cannot collect output: 'stdout' is locked",
      );
    }
    if (this.#stderr?.locked) {
      throw new TypeError(
        "Cannot collect output: 'stderr' is locked",
      );
    }

    const { 0: status, 1: stdout, 2: stderr } = await SafePromiseAll([
      this.#status,
      collectOutput(this.#stdout),
      collectOutput(this.#stderr),
    ]);

    return {
      success: status.success,
      code: status.code,

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pick one strategy: either await child.output() alone, or read child.stdout yourself and collect chunks manually
  2. If a reader was acquired by mistake, call reader.cancel() / releaseLock() on the existing reader before calling output() — but note partial data already consumed is lost
  3. Restructure: pipe stdout yourself if you need incremental processing, then assemble the final Uint8Array from the chunks you collected

Example fix

// before
const child = cmd.spawn();
const reader = child.stdout.getReader();
const out = await child.output(); // TypeError: 'stdout' is locked

// after
const child = cmd.spawn();
const out = await child.output();
const text = new TextDecoder().decode(out.stdout);
Defensive patterns

Strategy: type-guard

Validate before calling

function canCollectOutput(child: Deno.ChildProcess): boolean {
  return !(child.stdout?.locked ?? false) && !(child.stderr?.locked ?? false);
}
// call before: if (canCollectOutput(child)) await child.output();

Type guard

const outputSafe = (child: Deno.ChildProcess): boolean =>
  child.stdout?.locked !== true && child.stderr?.locked !== true;

Try / catch

try { out = await child.output(); } catch (err) { if (err instanceof TypeError && err.message.includes("is locked")) { /* fall back to manual reader loop */ } else throw err; }

Prevention

When it happens

Trigger: Calling const reader = child.stdout.getReader() (or child.stdout.pipeTo(w)) and then awaiting child.output(); also calling child.output() twice after the first call locked/consumed the stream via a prior manual read.

Common situations: Starting to stream logs incrementally, then deciding to also await the combined output; helper functions that attach readers for progress display while the main path calls output(); calling output() as a fallback after a partial pipe fails.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/e06136a16a9139ce. Report an issue: GitHub.