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
- Release the lock before transferring: reader.releaseLock() (and cancel/finish any pipeTo) so isReadableStreamLocked is false.
- Transfer the stream before anything reads it — move it to the Worker first and read there.
- 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
- Transfer streams before reading them; read in the receiving realm.
- Always pair getReader() with releaseLock() (or cancel) in a finally block.
- Remember pipeTo/pipeThrough and tee() also lock a readable stream.
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
- DataCloneError
- DataCloneError
- The BYOB request's buffer has been detached and so cannot be
- Closed already requested.
- ReadableByteStreamController's stream is not in a readable s
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/0719c7f518614ca2.
Report an issue: GitHub.