Migrating from Deno
Deno is the closest starting point — Web APIs, and permissions on the command line. Two things move: the Deno.* namespace becomes runtime: imports, and resolution becomes package.json plus node_modules rather than deno.json.
1. File operations (Deno.readTextFile, Deno.writeTextFile)
// ❌ Deno const text = await Deno.readTextFile("./data.txt"); await Deno.writeTextFile("./out.txt", "content"); await Deno.remove("./temp.txt"); // ✅ esrun import { file, write, remove } from "runtime:fs"; const text = await file("./data.txt").text(); await write("./out.txt", "content"); await remove("./temp.txt");
2. Process and environment (Deno.env, Deno.args)
// ❌ Deno const port = Deno.env.get("PORT"); const cliArgs = Deno.args; const currentDir = Deno.cwd(); // ✅ esrun import { env, args, cwd } from "runtime:process"; const port = env.PORT; const cliArgs = args; const currentDir = cwd();
3. Subprocesses (Deno.Command)
// ❌ Deno const command = new Deno.Command("git", { args: ["status"] }); const { code, stdout } = await command.output(); // ✅ esrun import { Command } from "runtime:system"; const { code, stdout } = await new Command("git", { args: ["status"], }).output();
4. Cookies (@std/http/cookie)
runtime:http has a cookie API built in. It follows Bun's rather than @std/http's: a CookieMap on the request instead of functions over Headers.
// ❌ Deno import { getCookies, setCookie, deleteCookie } from "jsr:@std/http/cookie"; Deno.serve((req) => { const { sid } = getCookies(req.headers); const headers = new Headers(); setCookie(headers, { name: "theme", value: "dark", sameSite: "Lax" }); deleteCookie(headers, "old"); return new Response("ok", { headers }); }); // ✅ esrun import { serve } from "runtime:http"; serve((req) => { const sid = req.cookies.get("sid"); // null, not undefined, when absent req.cookies.set("theme", "dark", { sameSite: "Lax" }); req.cookies.delete("old"); return new Response("ok"); });
@std/http/cookie | esrun |
|---|---|
getCookies(headers) | req.cookies, or new CookieMap(headers.get("cookie") ?? "") |
setCookie(headers, cookie) | req.cookies.set(name, value, options) |
deleteCookie(headers, name, attrs) | req.cookies.delete(name, attrs) |
getSetCookies(headers) | headers.getSetCookie().map((h) => Cookie.parse(h)) |
Behaviour differs in four ways:
Encoding: values are percent-encoded, where
@stdthrows on a space.Duplicates: the first of a duplicated name wins, where
@stdkeeps the last.Defaults: cookies default to
Path=/; SameSite=Lax.__Host-cookies: one that breaks its rules throws, where@stdquietly rewrites it into a valid one.
sameSite takes "Lax" as well as "lax".
5. Async context (node:async_hooks)
// ❌ Deno import { AsyncLocalStorage } from "node:async_hooks"; const store = new AsyncLocalStorage(); store.run({ tenant }, () => handle(req)); store.getStore().tenant; // ✅ esrun import { createContext } from "runtime:context"; const store = createContext({ name: "request" }); store.run({ tenant }, () => handle(req)); store.get().tenant;
| Deno | esrun |
|---|---|
new AsyncLocalStorage() | createContext({ name }) |
.getStore() | .get() |
.run(store, fn) | .run(value, fn) |
.enterWith() | Removed — snapshot() |
6. Observability and process metrics
Deno exports OpenTelemetry from the runtime rather than from the program, and so does esrun — the flag is --otel instead of an environment variable, and it needs no unstable gate.
// ❌ Deno // OTEL_DENO=1 deno run --unstable-otel app.ts Deno.memoryUsage().rss; // ✅ esrun // esrun --otel=http://localhost:4318 app.js import { memoryUsage, cpuTime, uptime } from "runtime:process"; import { metrics } from "runtime:diagnostics";
| Deno | esrun | Needs |
|---|---|---|
OTEL_DENO=1 --unstable-otel | --otel=<url> | — |
Deno.memoryUsage().heapUsed | memoryUsage().heapUsed | — |
Deno.memoryUsage().rss | metrics().process.rss | diagnostics |
| — | cpuTime() | — |
| — | metrics().gc, metrics().loopLagMs | diagnostics |
Deno.metrics() (removed in Deno 2) | metrics(), inventory() | diagnostics |
Reading your own telemetry is a capability here. Exporting is not: --otel is a deployment's decision and grants the program nothing, while subscribe() and metrics() need --allow-diagnostics and attributes additionally need --allow-diagnostics-detail.
Per-agent rather than per-process is the other difference: memoryUsage(), cpuTime() and uptime() describe the calling agent, so a worker answers for itself. metrics().process is the process-wide half.
See the Request context and Observing the runtime guides.