denoland/deno · error · TypeError
ReadableStream is locked
Error message
ReadableStream is locked
What it means
On the streaming-response path, when the response body is backed by an op resource (getReadableStreamResourceBacking returns non-null, e.g. a file or socket-backed stream), Deno must acquire the JS-side stream lock with getReader() to hand the resource to op_http_write_resource. If respBody.locked is already true someone else holds a reader and the transfer cannot proceed.
Source
Thrown at ext/http/01_http.js:300
) {
await respBody.cancel(error);
}
throw error;
}
if (isStreamingResponseBody) {
let success = false;
if (
respBody === null ||
!ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, respBody)
) {
throw new TypeError("Unreachable");
}
const resourceBacking = getReadableStreamResourceBacking(respBody);
let reader;
if (resourceBacking) {
if (respBody.locked) {
throw new TypeError("ReadableStream is locked");
}
reader = respBody.getReader(); // Acquire JS lock.
try {
await op_http_write_resource(
writeStreamRid,
resourceBacking.rid,
);
if (resourceBacking.autoClose) core.tryClose(resourceBacking.rid);
readableStreamClose(respBody); // Release JS lock.
success = true;
} catch (error) {
const connError = httpConn[connErrorSymbol];
if (
ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error) &&
connError != null
) {
// deno-lint-ignore no-ex-assign
error = new connError.constructor(connError.message);View on GitHub (pinned to 89f33cbef2)
Solutions
- Don't take a reader on the body you pass to respondWith; let Deno stream it.
- If you need to observe the stream, read it fully into memory or tee and consume only one branch.
- For resource-backed streams (files, sockets), prefer returning the stream directly in new Response(stream) and avoid any intermediate reader/lock.
- Release locks you no longer need: reader.releaseLock() before responding (only if no read is pending).
Example fix
// before const body = file.readable; const reader = body.getReader(); // lock held httpConn.respondWith(new Response(body)); // after const body = file.readable; httpConn.respondWith(new Response(body)); // no reader taken
Defensive patterns
Strategy: validation
Validate before calling
const body = resp.body;
if (body instanceof ReadableStream && body.locked) {
throw new Error("response body stream is locked; recreate the stream before responding");
} Type guard
function isUnlockedStream(s: unknown): s is ReadableStream { return s instanceof ReadableStream && !s.locked; } Try / catch
try { await httpConn.respondWith(new Response(stream)); } catch (e) { if (e instanceof TypeError && e.message === "ReadableStream is locked") { httpConn.close(); return; } throw e; } Prevention
- Don't call getReader()/pipeTo()/tee() on streams destined for the response.
- For file/socket streams, pass Deno.open(...).readable directly into new Response().
- On request cancellation, recreate streams instead of re-serving disturbed ones.
When it happens
Trigger: Calling response.body.getReader() (or pipeThrough/pipeTo, or response.body.tee()) and not releasing it before respondWith; piping a resource-backed stream (from Deno.open / Deno.stdin / a socket) into the Response while a consumer still holds a reader; teeing a resource-backed stream (tee locks and reads it).
Common situations: Progress meters that attach a reader to file/socket streams; combining tee() with streaming responses; duplex piping where one side keeps the reader; re-serving a stream after a cancelled request without recreating it.
Related errors
- Body is unusable
- Invalid header name: "${name}"
- Invalid header value: "${value}"
- Cannot construct MultipartParser: multipart/form-data must p
- Body already consumed
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/6398a7af01d04248.
Report an issue: GitHub.