"The script will never generate a response" in Workers
Error 1101 — The script will never generate a response is what Cloudflare’s frontend shows for a request whose Worker never handed back a Response, and it is a label rather than a diagnosis: at least four different branches end in it, and the one most JavaScript developers actually hit does not print that sentence anywhere in your logs. If your handler does return — with undefined, with a bare Response.json(…) expression you forgot to return, with a null that came out of a refactor — the runtime does not wait, does not cancel, and does not say “never generate a response”. It throws a TypeError naming the type it wanted, roughly one millisecond after the request arrived, and the client gets an error page. That string is the thing to search for; this article is the map from the message you were given to the line you have to change.
Everything quoted below was executed on one arm64 Linux 6.17 host with the real Workers runtime, workerd 2026-09-15, compatibilityDate = "2026-09-01". The harness runs one runtime process per route so a log line can never be attributed to the wrong request, and the transcript is that harness’s stdout, unedited. The platform-side claims (error codes, the wording of the 1101 message, the documented causes) are cited from Cloudflare’s Errors and exceptions reference, last updated 30 September 2026, and labelled as such where they cannot be reproduced without a Cloudflare account.
TL;DR — which branch are you in?
- One message, two engine branches. A handler that returns a value which is not a
Responseand is not a Promise fails withTypeError: Callback returned incorrect type; expected 'Promise'. A handler that returns a Promise resolving to anything else fails withTypeError: Incorrect type for Promise: the Promise did not resolve to 'Response'.Both measured, both verbatim. - The type failure is instant. Every non-Response route answered in 0.8–1.4 ms on this host. If your 1101s come back fast, you are in this branch; if the request sat there for seconds, you are in the hang branch instead (the “code had hung” case, measured here on 15 September).
- A
try/catcharound your handler body does not catch it. Measured: thecatchblock never runs, because the check happens after your function has already returned. ctx.waitUntil()does not exempt you either. Pending work in the background does not stand in for a returnedResponse.- The client sees almost nothing. From a local runtime: HTTP
500, bodyInternal Server Error, 21 bytes. Behind Cloudflare the same exception becomes the documented 1101 page. - The fix is one boundary guard —
value instanceof Response ? value : 502— which turns an opaque 1101 into a 502 you can grep, with the handler’s own throws still mapped to a 500. - A sibling error is easy to confuse with this one:
TypeError: Response with null body status (101, 204, 205, or 304) cannot have a body.The empty-body formnew Response(null, { status: 204 })answers204with 0 bytes (measured).
What the message covers, and what the runtime prints instead
The reference page is explicit about the platform-level symptom, and it is worth reading in full because the wording describes the event loop, not the return value:
Some requests may return a 1101 error with
The script will never generate a responsein the error message. This occurs when the Workers runtime detects that all the code associated with the request has executed and no events are left in the event loop, but a Response has not been returned.— Cloudflare, Errors and exceptions (last updated 30 September 2026)
Two documented causes sit under that sentence. The first is an unresolved Promise — code that awaits something which is never resolved or rejected — and the page recommends the no-floating-promises rule from typescript-eslint to catch Promises created and not handled. The second is a WebSocket whose server-side connection is never closed; with the web_socket_auto_reply_to_close compatibility flag, enabled by default on compatibility dates on or after 2026-04-07, the runtime completes the close handshake for you, so that branch mostly affects Workers on older dates.
Neither of those is the branch that a missing return lands in. When a handler completes and hands the runtime a value of the wrong type, the runtime does not consult the event loop — it has an answer, it just cannot use it — and it reports the type error. The table is the whole article in four rows; the rest of it is the evidence and the fix.
| What your handler did | Runtime log line (measured, workerd 2026-09-15) | Client |
|---|---|---|
Returned a value that is not a Response (sync path, including undefined, null, {}, a string, a number) | TypeError: Callback returned incorrect type; expected 'Promise' | 500 / 1101 |
Returned a Promise (or an async call) that resolved to something other than a Response | TypeError: Incorrect type for Promise: the Promise did not resolve to 'Response'. | 500 / 1101 |
| Threw, or rejected | Error: <your message> | 500 / 1101 |
| Nothing settled at all: the code finished and the event loop emptied with no Response | The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. … | 500 / 1101 |
The fourth row is the one that literally contains the phrase you were looking for, and it belongs to the branch where the runtime has to decide that nothing more is coming. Its full text, as shipped inside the workerd binary, is:
$ strings /path/to/node_modules/.bin/workerd | grep -i "would never generate"
The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/
So the triage is: a fast failure is a type error, a slow one is a hang, and only the hang is described by the message Cloudflare put in front of you. Both are errors in your code; they are fixed in different places.
Reproduce all of it in two minutes with workerd
workerd is the open-source Workers runtime — the same engine that serves your production traffic — and it installs without an account (npm install workerd, one ~160 MB binary). Point it at a Cap’n Proto config, bind a socket, and you can measure runtime behaviour instead of collecting screenshots from forums. The harness below writes a worker with eleven routes, starts the runtime once per route, and prints the client result next to the exception line from that process’s log.
mkdir -p /tmp/noresp && cd /tmp/noresp
WORKERD="${WORKERD:-/opt/data/c2cz/lab/20260915-hang/node_modules/.bin/workerd}"
[ -x "$WORKERD" ] || WORKERD="$PWD/node_modules/.bin/workerd" # npm install workerd
cat > config.capnp <<'CAPNP'
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [ (name = "main", worker = (
modules = [ (name = "worker.js", esModule = embed "worker.js") ],
compatibilityDate = "2026-09-01",
)) ],
sockets = [ (name = "http", address = "127.0.0.1:19940", http = (), service = "main") ],
);
CAPNP
cat > worker.js <<'JS'
export default {
fetch(request, env, ctx) {
const path = new URL(request.url).pathname;
if (path.startsWith("/guarded/")) return guard(() => buggy(path.slice(8), request));
return buggy(path, request);
},
};
function buggy(path, request) {
switch (path) {
case "/ok":
return new Response("ok\n");
case "/forgot": // the classic: an expression, no `return`
Response.json({ ok: false }, { status: 404 });
break;
case "/try-catch": // catch only sees throws from your own code
try {
if (request.method === "POST") return new Response("posted\n");
} catch (err) {
return new Response(`caught: ${err.message}\n`, { status: 500 });
}
break;
case "/async":
return (async () => { await new Promise((r) => setTimeout(r, 20)); })();
case "/throws":
throw new Error("lab: handler threw");
case "/204-with-body":
return new Response("body", { status: 204 });
case "/204-null":
return new Response(null, { status: 204 });
default:
return new Response("unknown\n", { status: 404 });
}
}
async function guard(thunk) {
let value;
try {
value = await thunk();
} catch (err) {
return new Response(`handler threw: ${err.message}\n`, { status: 500 });
}
if (value instanceof Response) return value;
return new Response(`handler returned ${typeof value} instead of a Response\n`, {
status: 502,
headers: { "content-type": "text/plain; charset=utf-8" },
});
}
JS
for R in /ok /forgot /try-catch /async /throws /204-with-body /204-null \
/guarded/forgot /guarded/try-catch /guarded/async /guarded/throws /guarded/ok; do
"$WORKERD" serve config.capnp > one.log 2>&1 &
SRV=$!
for i in $(seq 1 60); do
curl -s -o /dev/null --max-time 1 http://127.0.0.1:19940/ok && break
sleep 0.25
done
OUT=$(curl -sS -o body.txt -w "%{http_code} %{size_download}B" --max-time 10 "http://127.0.0.1:19940$R")
echo "=== $R"
echo "client $OUT body='$(cat body.txt)'"
MSG=$(grep -m1 "Uncaught exception" one.log | sed 's|.*failed: remote\.jsg\.||')
echo "log ${MSG:-(no uncaught exception)}"
kill -9 "$SRV" 2>/dev/null
wait "$SRV" 2>/dev/null
sleep 0.3
done
Its stdout on this host:
=== /ok
client 200 3B body='ok'
log (no uncaught exception)
=== /forgot
client 500 21B body='Internal Server Error'
log TypeError: Callback returned incorrect type; expected 'Promise'
=== /try-catch
client 500 21B body='Internal Server Error'
log TypeError: Callback returned incorrect type; expected 'Promise'
=== /async
client 500 21B body='Internal Server Error'
log TypeError: Incorrect type for Promise: the Promise did not resolve to 'Response'.
=== /throws
client 500 21B body='Internal Server Error'
log Error: lab: handler threw
=== /204-with-body
client 500 21B body='Internal Server Error'
log TypeError: Response with null body status (101, 204, 205, or 304) cannot have a body.
=== /204-null
client 204 0B body=''
log (no uncaught exception)
=== /guarded/forgot
client 502 49B body='handler returned undefined instead of a Response'
log (no uncaught exception)
=== /guarded/try-catch
client 502 49B body='handler returned undefined instead of a Response'
log (no uncaught exception)
=== /guarded/async
client 502 49B body='handler returned undefined instead of a Response'
log (no uncaught exception)
=== /guarded/throws
client 500 34B body='handler threw: lab: handler threw'
log (no uncaught exception)
=== /guarded/ok
client 200 3B body='ok'
log (no uncaught exception)
Read the three blocks of results together and the shape of the bug is visible. /forgot, /try-catch and a plain return undefined; all produce the same first message. /async — an async branch that awaits and then returns nothing — produces the second one, which is why two teams with the same symptom can quote different strings at each other. And every /guarded/… route is answerable: the guard decides what the client gets, and the log stays clean.
Two details in that transcript are worth more than they look. First, the client body is Internal Server Error in a local run: there is no Cloudflare frontend here to render the branded 1101 page, so the HTTP status you see locally (500) and the status a real user sees (1101) differ while the engine behaviour does not. Second, /204-null is the control for the sibling error: a 204 with a body is a TypeError, a 204 with a null body is a perfectly good empty response.
Branch A: the handler returned, but not a Response
This is the branch that reaches production most often, because the mistake is a single missing keyword and the code reads correctly at a glance. Every one of these returns the same measured line:
Response.json({ ok: false }, { status: 404 });as a statement, with noreturnin front of it. The call still runs; the object it built is discarded.- A
switch(orif/else ifchain) whose last arm falls through, so the function ends without areturn. - A helper that returns
Response | undefined, called asreturn helper(...)by some callers and ignored by others. - A handler that returns a plain object —
{ status: 200, body: "ok" }— which is a perfectly good shape for a test double and not aResponsefor the runtime. ctx.waitUntil(promise)followed by noreturn: the background work is registered, the handler still returnsundefined, and the runtime still refuses.
The message is worth reading closely, because it explains something that looks wrong: it says expected 'Promise' even for a handler that is not async. The fetch callback’s contract in the runtime is “a Promise of a Response”; a handler that returns a Response outright is coerced into that contract, and a handler that returns anything else fails the check before the coercion can happen. The string tells you the shape the callback layer wanted, not the shape you forgot to build.
This is also the branch where a defensive try/catch gives false comfort. Measured: a handler wrapped in try { … } catch (err) { return new Response(…, { status: 500 }) } whose try body completes without returning produces the exact same TypeError, and the catch block never executes. There is nothing for catch to see: your code did not throw, it returned the wrong thing, and the type check happens one layer above you.
Branch B: the Promise resolved to something else
Move the same mistake behind one await and the wording changes. All four of these produce TypeError: Incorrect type for Promise: the Promise did not resolve to 'Response'.:
return (async () => { await doWork(); })();— an inline async branch with no return value.return Promise.resolve(undefined);, or any helper whose happy path forgot itsreturn.- An object with a
then()method that resolves toundefined(the runtime awaits it like any other Promise). - The middleware refactor:
const response = await next(request);and noreturn response;on the next line. The downstream handler built a beautifulResponse; the middleware dropped it on the floor.
The last one deserves its own paragraph because it does not look like a bug in review. Middleware is usually written as a chain of async functions, each of which is supposed to pass the value it awaited back up the chain. Delete one return — during a TypeScript migration, say, when a type annotation is added but the body is left alone — and every route that passes through that layer answers 1101 while the handler underneath keeps returning 200s in your unit tests. The unit tests call the handler; they do not call the chain.
Three “fixes” that are not fixes
- A
try/catcharound the handler body. Measured above: thecatchnever runs. It does not catch a wrong-type return because nothing was thrown. ctx.waitUntil()as a way to “keep the request alive”. Measured: it changes nothing about what the handler must return. It is for work that should outlive the response, not a substitute for one.no-floating-promisesas the whole answer. The rule is recommended by the same reference page and it will find Promises you created and never handled — which is the hang branch. It has nothing to say about a function whose declared return type isResponseand whose body returns nothing in one branch.
What does work, in increasing order of strength: make the return type explicit (Promise<Response> on every handler and every middleware step, so the compiler refuses an undefined arm); give every switch a default that returns; and put one boundary guard in front of everything, because the compiler only protects code you type-check, and the runtime error arrives in production anyway when a string of any gets in the way.
The guard: turn an opaque 1101 into a 502 you can grep
One wrapper, applied once at the module’s fetch export, removes the whole class of failure from the user-visible surface. It also gives you the log line the platform’s error page was hiding: the path that returned the wrong thing, next to the exception you raised yourself.
// worker.js - the boundary guard: one place decides what a handler may return.
export function withResponseGuard(handler) {
return async function (request, env, ctx) {
let value;
try {
value = await handler(request, env, ctx);
} catch (err) {
// YOUR throws land here: log the stack, answer with a real Response
console.error("handler threw:", err && err.stack);
return new Response("Internal Server Error\n", {
status: 500,
headers: { "content-type": "text/plain; charset=utf-8" },
});
}
if (value instanceof Response) return value;
console.error("handler returned no Response:", new URL(request.url).pathname);
return new Response("Bad Gateway\n", {
status: 502,
headers: { "content-type": "text/plain; charset=utf-8" },
});
};
}
export default {
fetch: withResponseGuard(async (request) => {
switch (new URL(request.url).pathname) {
case "/health":
return Response.json({ ok: true });
case "/session":
return loadSession(request); // returns a Response, or nothing
default:
return new Response("not found\n", { status: 404 });
}
}),
};
async function loadSession(request) {
if (!request.headers.get("authorization")) {
return new Response("Unauthorized\n", { status: 401 });
}
// ...and on the happy path the refactor dropped the return
}
Save it as worker.js (or guard.mjs if you want node --check to accept the ESM syntax locally). Two design choices are deliberate. The 502 says “the gateway could not get a valid response from the handler”, which is what actually happened, and it keeps 500 meaning “your handler threw” — the branch the wrapper still maps for you, with a body you control instead of the platform’s. And the check is instanceof Response, not a truthiness test: an empty Response, a 204, and a Response with a ReadableStream body are all truthy and all valid, while {} and a non-empty string are truthy and invalid.
Because the guard is one function, it is also the place to assert in development. In a Vitest suite for a Worker — the one-workerd-per-file setup measured here in September — you can call every route and assert expect(value).toBeInstanceOf(Response) on the raw handler, before the guard normalises anything. That test fails on the machine of whoever breaks it, instead of on the error page of whoever uses it.
Triage checklist
- Read the exception, not the error page. In the dashboard, 1101 means “Worker threw a JavaScript exception” and the message in the reference’s error-code table is a category, not a cause.
wrangler tailprints the real one under theexceptionsfield of its JSON output. - Time the failure. Sub-millisecond failures are the type branches; a request that waited is the hang branch or the WebSocket branch.
- Search for
Response.on a line that does not start withreturn. The bareResponse.json(…)statement is the single most common cause. It is a manual review, not a linter result: a regex cannot tell a dropped return from aResponsethat is being awaited into a variable on purpose. - Check the middleware chain. Every
await next(...)must be returned, in every layer, including the ones added during a refactor. - Check your compatibility date if WebSockets are involved. On dates before 2026-04-07 the auto-close flag is off and an unclosed server-side socket is a documented cause of the same 1101.
- Then add the guard and keep watching the 502s for a week — that count is the number of requests you would otherwise have served as an error page.
Sources and related work
- Measured on this host: arm64 Linux 6.17.0,
workerd2026-09-15 withcompatibilityDate = "2026-09-01", curl 8.14.1. Every transcript above is that harness’s stdout; the log lines are quoted from the runtime, including theUncaught exceptionprefix the runner strips withsed. - Cloudflare, Errors and exceptions (last updated 30 September 2026): the 1101 error-code table, the quoted paragraph on “The script will never generate a response” errors, both documented causes, the
web_socket_auto_reply_to_closenote, and thewrangler tailguidance: developers.cloudflare.com/workers/observability/errors/. - typescript-eslint, no-floating-promises: typescript-eslint.io/rules/no-floating-promises/.
- Related work on this blog: “code had hung” Error 1101: the cancel branch, Error 1101 when the Worker throws, and the runtime’s request-ownership error, which uses the same
workerdharness.