The error, verbatim
Jump to the fix ↓Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'jsr:'
Tested on
- Node
- 26.11.1
- Wrangler
- 4 (wrangler dev, local)
- Deno
- 2.9.6 (baseline only)
- Re-run
- November 3: Deno 2.9.8, Node 26.13.0, wrangler 4.156.2 (Linux)
- OS
- Windows 11 Pro; Ubuntu on GitHub Actions
✓ Last re-run on GitHub Actions · see the run · all repros
Contents
On October 9, 2026, the Deno team announced it’s joining Cloudflare. Deno Deploy will keep running for six months and then shut down, which puts the end around April 2027. The Deno runtime gets one more year of monthly bug-fix and security releases. JSR stays up, moving to Cloudflare’s infrastructure. Neither announcement includes a migration guide; Deno offers migration help only to paying customers moving to Workers.
So I took the kind of app Deno Deploy is made for and moved it. It uses the four things almost every Deno Deploy app uses: Deno.serve, Deno.env, Deno KV and a jsr: import:
import { Hono } from "jsr:@hono/hono@4";
const kv = await Deno.openKv();
const app = new Hono();
app.get("/", (c) => c.text(`hello from ${Deno.env.get("APP_NAME") ?? "deno"}`));
app.get("/count", async (c) => {
const cur = (await kv.get<number>(["visits"])).value ?? 0;
await kv.set(["visits"], cur + 1);
return c.json({ visits: cur + 1 });
});
Deno.serve({ port: 8787 }, app.fetch);
On Deno 2.9.6 it worked when run with --unstable-kv (without that flag, Deno itself fails with Deno.openKv is not a function). Run unchanged anywhere else, it fails before serving a single request:
| Running it on | First error |
|---|---|
| Node 26 | ERR_UNSUPPORTED_ESM_URL_SCHEME … Received protocol 'jsr:' |
Cloudflare Workers (wrangler dev) |
Could not resolve "jsr:@hono/hono@4" and Top-level await is currently not supported with the "iife" output format |
Step 1, both targets: replace jsr: imports
Neither Node nor Wrangler understands jsr: URLs. JSR publishes every package to an npm-compatible registry, and its CLI adds them to package.json:
npx jsr add @hono/hono@4Then import by name, without the jsr: prefix:
import { Hono } from "@hono/hono";With the import fixed, the Deno global is next. On Node 26: ReferenceError: Deno is not defined. On Workers the build still stops at the top-level await Deno.openKv().
Moving to Node 26
Three replacements: Deno.serve → @hono/node-server, Deno.env.get → process.env, and Deno.openKv → the official @deno/kv npm package, which has the same API and stores data in a local SQLite file:
npm i @hono/node-server @deno/kvimport { Hono } from "@hono/hono";
import { serve } from "@hono/node-server";
import { openKv } from "@deno/kv";
const kv = await openKv("kv.sqlite");
const app = new Hono();
app.get("/", (c) => c.text(`hello from ${process.env.APP_NAME ?? "node"}`));
app.get("/count", async (c) => {
const cur = (await kv.get<number>(["visits"])).value ?? 0;
await kv.set(["visits"], cur + 1);
return c.json({ visits: cur + 1 });
});
serve({ fetch: app.fetch, port: 8787 });The data lives in kv.sqlite on that one machine. That’s fine for a single server, not for several. Your existing Deno KV data stays on Deno Deploy, so export it while Deploy is still running.
Moving to Cloudflare Workers
A Worker doesn’t start a server. It exports a fetch handler, gets settings and storage through env bindings, and can’t run await at the top level.
Export the app instead of serving it, read settings from c.env, and use Workers KV:
import { Hono } from "@hono/hono";
type Env = { APP_NAME?: string; KV: KVNamespace };
const app = new Hono<{ Bindings: Env }>();
app.get("/", (c) => c.text(`hello from ${c.env.APP_NAME ?? "workers"}`));
app.get("/count", async (c) => {
const cur = Number((await c.env.KV.get("visits")) ?? 0);
await c.env.KV.put("visits", String(cur + 1));
return c.json({ visits: cur + 1 });
});
export default app;{
"name": "deno-app",
"main": "main.ts",
"compatibility_date": "2026-10-01",
"vars": { "APP_NAME": "workers" },
"kv_namespaces": [{ "binding": "KV", "id": "local-dev-only" }]
}Before deploying, create a real namespace with npx wrangler kv namespace create KV and put its id in place of local-dev-only.
What I ran, and when
Windows 11 Pro, October 14, 2026. The original app ran on Deno 2.9.6 (from npm, as a baseline). The ports ran on Node 26.11.1 and in Wrangler 4’s local wrangler dev, with nothing deployed to a Cloudflare account. Every error above is the real output.
On November 3, 2026, the whole move ran again on Linux (GitHub Actions ubuntu-latest) with Deno 2.9.8, Node 26.13.0 and wrangler 4.156.2, using wrangler dev only, so nothing was deployed. Every step matched: Deno needed --unstable-kv; the unchanged file failed on Node 26 with ERR_UNSUPPORTED_ESM_URL_SCHEME and in wrangler dev with the jsr: and top-level await errors; npx jsr add @hono/hono without a version still picked 5.0.0-rc.1, and with @4 it added ^4.13.16; and both migrated versions answered / and counted 1, then 2. The script and the run are public.
— N.K., end of entry No.066