denoland/deno · error · Deno.errors.Http
Already closed
Error message
Already closed
What it means
Before performing an upgrade, the serve request context checks that it is still live: once close() has run (response finished, request aborted, or handler completed), the native external handle is released (#external = null). Attempting an upgrade afterwards throws Deno.errors.Http 'Already closed' because the underlying connection is gone.
Source
Thrown at ext/http/00_serve.ts:233
this.#external = null;
}
get [_upgraded]() {
return this.#upgraded;
}
_throwIfUpgraded() {
if (this.#upgraded) {
throw new Deno.errors.Http("Already upgraded");
}
}
_wantsUpgrade(upgradeType) {
if (this.#upgraded) {
throw new Deno.errors.Http("Already upgraded");
}
if (this.#external === null) {
throw new Deno.errors.Http("Already closed");
}
if (upgradeType == "upgradeWebSocket") {
const external = this.#external;
this.url();
this.headerList;
this.remoteAddr;
this.close();
this.#upgraded = true;
return op_http_upgrade_websocket_next(external);
}
}
url() {
if (this.#urlValue !== undefined) {View on GitHub (pinned to 89f33cbef2)
Solutions
- Perform the upgrade synchronously during request handling, before any awaits that can complete the response
- Branch on the upgrade header early and return the upgrade response from that branch
- Catch Deno.errors.Http for late upgrades and treat it as a lost connection, not a crash
Example fix
// before
async function handler(req, info) {
await doSlowWork();
return info.upgradeWebSocket(req).response; // Already closed
}
// after
async function handler(req, info) {
if (wantsWs(req)) {
const { socket, response } = info.upgradeWebSocket(req); // upgrade first
doSlowWork().then(() => socket.send("ready"));
return response;
}
await doSlowWork();
return new Response("ok");
} Defensive patterns
Strategy: try-catch
Try / catch
try {
const { socket, response } = info.upgradeWebSocket(req);
} catch (err) {
if (err instanceof Deno.errors.Http && err.message === "Already closed") {
// request finished before upgrade; nothing to salvage, close quietly
return new Response("upgrade too late", { status: 400 });
} else throw err;
} Prevention
- Upgrade synchronously during request handling, before awaits that can finish the response
- Branch on the Upgrade header first and return the upgrade response immediately
- Treat 'Already closed' as a lost connection, not a retriable error
When it happens
Trigger: Awaiting something (a promise, timeout, or another request) inside the handler and only then calling upgradeWebSocket, after the response was already committed or the request was closed; upgrading in a detached async continuation after the handler returned.
Common situations: Async handler flows that respond first and upgrade later; cleanup/timeout races where the framework closed the request; websocket fallback logic that runs too late; long-running handlers whose client disconnected first.
Related errors
- Already upgraded
- Request closed
- Invalid response
- Return value from serve handler must be a response or a prom
- Return value from serve handler must be a Response construct
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/a2c00e7212d56467.
Report an issue: GitHub.