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)

JavaScript
// ❌ 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)

JavaScript
// ❌ 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)

JavaScript
// ❌ 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();

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.

JavaScript
// ❌ 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/cookieesrun
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 @std throws on a space.

  • Duplicates: the first of a duplicated name wins, where @std keeps the last.

  • Defaults: cookies default to Path=/; SameSite=Lax.

  • __Host- cookies: one that breaks its rules throws, where @std quietly rewrites it into a valid one.

sameSite takes "Lax" as well as "lax".

5. Async context (node:async_hooks)

JavaScript
// ❌ 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;
Denoesrun
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.

JavaScript
// ❌ 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";
DenoesrunNeeds
OTEL_DENO=1 --unstable-otel--otel=<url>—
Deno.memoryUsage().heapUsedmemoryUsage().heapUsed—
Deno.memoryUsage().rssmetrics().process.rssdiagnostics
—cpuTime()—
—metrics().gc, metrics().loopLagMsdiagnostics
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.

Last updated on
Edit this page