denoland/deno · error · DOMException

DataCloneError

DataCloneError

Error message

Cannot transfer a locked ReadableStream

What it means

DataCloneError DOMException thrown by readableStreamTransferSteps, the structured-clone serializer hook for ReadableStream (ext/web/06_streams.js). Transferring a ReadableStream via postMessage/structuredClone pipes its contents across the MessagePort, which requires taking a reader; if the stream is already locked (active reader, ongoing pipeTo, or tee), the transfer is refused.

Source

Thrown at ext/web/06_streams.js:7892

  setUpWritableStreamDefaultController(
    stream,
    controller,
    startAlgorithm,
    writeAlgorithm,
    closeAlgorithm,
    abortAlgorithm,
    1,
    sizeAlgorithm,
  );
}

/**
 * @param value {ReadableStream<any>}
 * @param port {MessagePort}
 */
function readableStreamTransferSteps(value, port) {
  if (isReadableStreamLocked(value)) {
    throw new DOMException(
      "Cannot transfer a locked ReadableStream",
      "DataCloneError",
    );
  }
  const writable = new WritableStream(_brand);
  setUpCrossRealmTransformWritable(writable, port);
  const promise = readableStreamPipeTo(value, writable, false, false, false);
  setPromiseIsHandledToTrue(promise);
}

/**
 * @param port {MessagePort}
 * @returns {ReadableStream<any>}
 */
function readableStreamTransferReceivingSteps(port) {
  const stream = new ReadableStream(_brand);
  setUpCrossRealmTransformReadable(stream, port);
  return stream;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Release the lock before transferring: reader.releaseLock() (and cancel/finish any pipeTo) so isReadableStreamLocked is false.
  2. Transfer the stream before anything reads it — move it to the Worker first and read there.
  3. If you need the data in both places, tee() first and transfer only the branch you do not consume locally... instead, cancel the local branch after tee and transfer the other.

Example fix

// before
const reader = res.body.getReader();
reader.read();
worker.postMessage(res.body, [res.body]); // DataCloneError: locked

// after
const body = res.body;
worker.postMessage(body, [body]); // transfer first, unlocked
// read it inside the worker
Defensive patterns

Strategy: validation

Validate before calling

// readable.locked mirrors the internal isReadableStreamLocked check.
if (stream.locked) {
  // release or cancel before transfer
  // reader.releaseLock();  await stream.cancel();  or finish pipeTo
}
worker.postMessage(stream, [stream]);

Try / catch

try {
  worker.postMessage(stream, [stream]);
} catch (err) {
  if (err instanceof DOMException && err.name === 'DataCloneError') {
    throw new Error('ReadableStream still locked; release the reader before transferring');
  }
  throw err;
}

Prevention

When it happens

Trigger: postMessage(readableStream, [readableStream]) or structuredClone(rs, { transfer: [rs] }) while a getReader() is outstanding, a pipeTo/pipeThrough is in progress, or the stream was tee'd — all of which hold the lock.

Common situations: Sending a fetch response body to a Worker after starting to read it in the main thread; transfer code that forgot releaseLock() on a reader; frameworks that implicitly lock streams (tee for inspection) before user transfer code runs.

Related errors


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