The Bug That Never Threw an Error

A fetch() call that worked everywhere except production. No errors, no logs, no clues — just silence.

Everything worked except the part that mattered.

I was wiring up a notification pipeline for a Cloudflare Worker — when a certain event fires, hit a webhook endpoint, send an email. Simple. The webhook worked fine when I tested it with curl. The Worker code looked correct. But in production, no notifications arrived. Ever.

No errors in the logs. No failed requests. Nothing.

The code looked fine

The Worker’s handler called fetch() to hit the webhook, then returned a Response to the client:

const response = buildResponse(result);
fetch(webhookUrl, { method: "POST", body: JSON.stringify(payload) });
return response;

The problem: Cloudflare Workers terminate the execution context the moment a Response is returned. That fetch() call? Fire-and-forget. The runtime killed the isolate before the request ever left the building.

No error thrown because the promise was never awaited. No log entry because the code never got far enough to produce one. The call just vanished.

One line, four commits

Wrap the fire-and-forget call in ctx.waitUntil():

ctx.waitUntil(fetch(webhookUrl, { method: "POST", body: JSON.stringify(payload) }));
return response;

waitUntil() tells the runtime: keep this isolate alive until this promise resolves, even after you’ve sent the response back to the client. The ExecutionContext (ctx) has to be threaded through from the Worker’s entry point to wherever you need background work.

That’s it.

Your runtime owes you nothing

This bug passed every test I threw at it. The webhook worked via curl. The Worker returned correct responses. No errors anywhere. The code was syntactically correct and logically wrong.

In serverless environments, your mental model of “when code stops running” is probably wrong. The function doesn’t wait for your unawaited promises to finish. It’s gone the instant the response ships. If you’re doing any work after returning a response — logging, notifications, cleanup — you need to explicitly ask the runtime to wait.

When something silently does nothing, check your assumptions about the execution lifecycle. The bug isn’t in the logic. It’s in when the logic runs.