Deno 2.8 is here. This is our biggest minor release to date and we’re excited to share it with you.
To upgrade to Deno 2.8, run the following in your terminal:
If Deno is not yet installed, run one of the following commands to install or learn how to install it here.
curl -fsSL https://deno.land/install.sh | sh
iwr https://deno.land/install.ps1 -useb | iexNew subcommands
deno audit fix
deno audit
(shipped in 2.6)
reports vulnerabilities in npm packages in your dependency tree. The new
deno audit fix subcommand goes one step further and automatically upgrades
affected packages to the nearest patched version that still satisfies your
version constraints (#32909,
#34273). The same behavior is
also available as a --fix flag on deno audit:
$ deno audit fix
╭ body-parser vulnerable to denial of service when url encoding is enabled
│ Severity: high
│ Package: body-parser
│ Vulnerable: <1.20.3
╰ Info: https://github.com/advisories/GHSA-qwcr-r2fm-qrc7
╭ Express.js Open Redirect in malformed URLs
│ Severity: moderate
│ Package: express
│ Vulnerable: <4.19.2
╰ Info: https://github.com/advisories/GHSA-rv95-896h-c2vc
Found 2 vulnerabilities
Severity: 0 low, 1 moderate, 1 high, 0 critical
Fixed 1 vulnerability:
body-parser 1.19.0 -> 1.20.3
1 vulnerability could not be fixed automatically:
express (major upgrade to 5.0.0)Anything that needs a major-version bump is listed separately, so you can decide
whether to relax the constraint.
Learn more about deno audit fix.
deno bump-version
deno bump-version updates the version field in your deno.json or
package.json (#30562):
$ deno bump-version patch
$ deno bump-version minor
$ deno bump-version major
$ deno bump-version prerelease In a workspace it does more. Run it at the workspace root and the same increment
is applied to every member package, with matching jsr: version constraints in
the root config and import map rewritten in place so cross-package references
stay in sync (#33689):
$ deno bump-version patch Without an increment argument, workspace mode switches to deriving per-package
bumps from Conventional Commits between
a base ref and the current branch. It honors scoped commits, wildcard *
scopes, BREAKING / ! for major bumps, prerelease increments, and 0.x.y
semver semantics, and treats any manual version edits since the base ref as
authoritative.
$ deno bump-version --base=main --dry-run--dry-run prints the planned changes without writing anything, and --start /
--base let you pin the comparison range when the default “current branch since
the latest tag” isn’t what you want.
Learn more about deno bump-version.
deno ci
CI scripts and Dockerfiles want one thing from an install: “give me exactly what
the lockfile says, and fail loudly if anything is off.” Until now that meant
remembering the right combination of flags on deno install. Deno 2.8 adds a
dedicated deno ci
subcommand (#34235):
It errors if deno.lock is missing, removes any existing node_modules
directory, and then runs the install with --frozen so the lockfile must match
the config file exactly. Drop it into your CI step or Dockerfile and you get
an obvious, greppable signal of “reproducible install” without having to think
about flags. --prod and --skip-types work the same way they do on
deno install.
deno pack
deno pack is closer to tsc + npm pack combined than to npm pack alone:
it builds a Deno or JSR project into an npm-publishable tarball in one shot
(#32139). Given a deno.json
like:
deno.json
{
"name": "@scope/my-lib",
"version": "1.0.0",
"exports": "./mod.ts"
}…running deno pack produces a scope-my-lib-1.0.0.tgz that’s ready for
npm publish. The tarball contains:
- A generated
package.jsonwithtype: "module", conditionalexports(types/import/default), and the extracted runtime dependencies. - Your TypeScript transpiled to JavaScript.
.d.tsdeclaration files extracted via the same fast-check pipelinedeno publishuses (pass--allow-slow-typesto skip).READMEandLICENSEfiles if present in the project root.
Along the way deno pack rewrites specifiers so the published package works
inside the npm ecosystem: jsr:@std/path becomes @jsr/std__path,
npm:express@4 becomes express, relative ./utils.ts imports become
./utils.js, and node: builtins are left alone. If your code calls Deno.*
APIs, the package automatically picks up @deno/shim-deno as a dependency so it
runs on Node too (opt out with --no-deno-shim).
File selection is graph-based: only modules reachable from your declared
exports are bundled, not whatever sits in the directory. Tarballs are
deterministic (sorted entries, fixed timestamps and permissions), which matters
for reproducible builds and content-addressed registries.
$ deno pack
$ deno pack --dry-run
$ deno pack --set-version 2.0.0
$ deno pack --output my-package.tgz
$ deno pack --ignore=tests/
$ deno pack --allow-dirty deno transpile
A new subcommand strips types from TypeScript, JSX, and TSX and writes plain JavaScript to disk. No bundling, no module rewriting, no config. Just the emit step.
greeter.ts
interface User {
name: string;
balance: number;
}
export function greet(user: User): string {
return `Hello ${user.name}, you have $${user.balance.toFixed(2)}`;
}$ deno transpile greeter.ts -o greeter.jsgreeter.js
export function greet(user) {
return `Hello ${user.name}, you have $${user.balance.toFixed(2)}`;
}deno transpile accepts multiple files, --outdir for batch output,
--source-map separate|inline, and --declaration to emit .d.ts alongside
the JS. Useful when you need to publish a JS-only artifact or pre-build TS for a
runtime that doesn’t speak it natively.
Learn more about deno transpile.
deno why
deno why <package> explains why a package is installed by walking from your
direct dependencies down to the package in question
(#32908). It’s the equivalent of
npm explain / pnpm why / yarn why. It works with both npm and JSR
dependencies (#34227).
Given a project that mixes both registries:
deno.json
{
"imports": {
"express": "npm:express@^4",
"dax": "jsr:@david/dax@^0.43"
}
}deno why traces an npm transitive back to its npm entry point:
$ deno why qs
[email protected]
npm:express@4 > [email protected]
[email protected]
npm:express@4 > [email protected] > [email protected]…and a JSR transitive back to its JSR entry point, with each path through the tree listed separately:
$ deno why @std/path
@std/[email protected]
jsr:@david/[email protected] > @std/[email protected]
jsr:@david/[email protected] > @david/[email protected] > @std/[email protected]
jsr:@david/[email protected] > @std/[email protected] > @std/[email protected]
jsr:@david/[email protected] > @david/[email protected] > @std/[email protected] > @std/[email protected]Pin to a specific version with deno why [email protected] or
deno why @std/[email protected] when you only care about one branch of the tree.
Learn more about deno why.
Deno now defaults to npm:
Deno 2.8 drops the npm: prefix requirement at the CLI: deno add and
deno install now treat unprefixed names as npm packages by default
(#33246), so the command you type
matches what every Node developer already types out of muscle memory.
$ deno add express
error: express is missing a prefix. Did you mean `deno install npm:express`?
$ deno add express
Add npm:[email protected]
Dependencies:
+ npm:[email protected]The npm: prefix still works (and is still required in import specifiers),
but you don’t have to type it at the CLI. JSR packages keep the jsr: prefix so
the two registries stay unambiguous.
With this change deno install becomes a drop-in for npm install, yarn, or
pnpm install in an existing Node project. It reads package.json, writes a
compatible node_modules layout, and
installs 3.66x faster than 2.7 on a cold cache; warm installs
are faster still thanks to Deno’s shared global cache across projects. Reach for
Deno as your package manager and keep running everything else on Node.
Learn more about deno install.
Node.js API compatibility
Node.js compatibility has been an important focus for us in the past couple
years. And we’re happy to announce that we made a huge leap forward in Deno 2.8:
pass rate against Node’s own test suite jumped from roughly 42% in Deno 2.7 to
76.4% in Deno 2.8 (3,405 of 4,457 tests passing); 500 commits landed since
Deno 2.7, touching nearly every node: module.
We keep close track of this percentage at node-test-viewer.deno.dev:
Head-to-head against Bun 1.3.14 on the same suite:
Node.js test suite pass rate (4,457 tests)
Deno v2.8
76.4% (3,405)
Bun 1.3.14
40.6% (1,810)
Excluding tests that bail out early: Deno 2.8 72.4% (3,229 / 4,457) vs Bun 1.3.14 36.4% (1,623 / 4,457).
Deno 2.8 also makes Node compatibility cheaper in real projects: many Node
built-in modules are now lazy-loaded, so programs that don’t touch them start
faster (importing one of those modules later pays a small deferred load cost).
Several node:* hot paths also picked up dedicated optimizations; see the
Performance section below for benchmark numbers.
Performance
Deno 2.8 ships meaningful speedups across the package manager, node:*
compatibility, HTTP serving, and the Web platform. Measured on Linux against
Deno 2.7.1:
Deno 2.7 (gray) vs 2.8 (blue)
Cold npm install
lower is better
3.66x faster
node:buffer base64
lower is better
3.07x faster
node:http throughput
higher is better
v2.7
8,339 req/s
v2.8
18,431 req/s
2.21x faster
node:crypto scrypt
lower is better
2.12x faster
node:http p99 latency
lower is better
v2.7
20.86 ms
v2.8
11.89 ms
1.75x faster
node:http chunked writes
higher is better
v2.7
6,635 req/s
v2.8
11,521 req/s
1.74x faster
Chunked writes p99
lower is better
v2.7
25.39 ms
v2.8
15.68 ms
1.62x faster
node:fs recursive cpSync
lower is better
1.49x faster
Worker MessagePort ping-pong
lower is better
v2.7
1,678 ms
v2.8
1,270 ms
1.32x faster
Bars share a scale per order of magnitude. Process benchmarks: 30 hyperfine samples. HTTP benchmarks: 10 samples of 30-second oha runs.
Cold npm installs. An entrypoint importing React, Vite, Babel parser, and
ESLint installs 3.66x faster in Deno 2.8, 3,319ms down to 906ms, on a
fresh DENO_DIR. Across 30 samples, the bootstrap 95% confidence interval for
the speedup was 3.53x to 3.75x. A few of the changes that fed into that
number:
- Abbreviated packuments
(#32364). The npm registry
exposes a smaller “abbreviated” metadata document
(
application/vnd.npm.install-v1+json) that includes only the fields a resolver needs; Deno now uses this smaller document for resolution and only fetches the full packument if it needs to. - Parallel npm resolution (#32416). The resolver used to walk parent nodes one at a time. Deno 2.8 fans out across parent nodes too, so independent branches of the dependency tree no longer wait on each other.
- Decompression off the async event loop (#32400). Large packument gzip decompression could stall other HTTP/2 streams sharing the same connection. Deno 2.8 routes registry-body decompression through a blocking thread pool, freeing the event loop for more concurrent requests.
- Tarball extraction split into CPU and I/O phases
(#32408). Tarball extract used
to be a single tight loop. It now splits into a CPU-bound decompression phase
and an I/O-bound filesystem write phase. Pairs with libdeflater + a
preallocated buffer (#32511)
(a faster gzip decoder than the stock
flate2) and fewer syscalls during tarball extraction (#32541).
node:http. Hello-world node:http more than doubles throughput (2.21x)
and cuts p99 latency by roughly 40%; chunked responses see comparable gains
(1.74x throughput, 1.62x tail latency).
base64 across the board. A single change, switching base64 encode/decode
to simdutf
(#32743), drives 3.07x faster
node:buffer base64 (2,594ms down to 844ms) and the same kind of speedup
for atob / btoa and every Web API path that touches base64.
Other node:* hot paths. scryptSync from node:crypto is now 2.12x
faster, Rust-backed recursive node:fs cpSync gets 1.49x faster, and
exchanging messages over MessagePort between Workers is now 1.32x faster.
Deno.serve. Native Deno.serve got a direct dispatch into the JS handler,
a fast path for fully-buffered response bodies, and lighter Vary handling
(#33845,
#33844,
#33892). A hello-world benchmark
sees 1.13x increase in throughput and 1.20x lower median p99 latency.
Other optimizations. A pile of smaller wins that show up everywhere:
TextEncoder/TextDecoderfast paths for ASCII / Latin-1 / short strings (#32735, #33674, #33675, #34055).- Linear-time
set/deleteonFormData,URLSearchParams, andHeaders(#33961), no more quadratic blowups on large header sets. URLPatternops drop serde overhead and GC pressure (#32766), so middleware that matches every request gets cheaper.- Zero-copy V8-to-Rust string conversion in op slow-paths
(#32688) and a SIMD ASCII fast
path for
op_decode(#33720), whichResponse.text(),File.text(), and FormData parsing all ride on. - V8 thread pool capped at 4 threads (#33697), trimming ~1 MB RSS on a typical desktop.
malloc_trimafter module loading (#32662) and on Worker termination (#32617), fixing 3–5x RSS bloat on Linux when loading large TypeScript codebases.
import defer
Deno now supports the TC39 import defer proposal: a module can be loaded and parsed without running its top-level code. The module is then only evaluated the first time you touch one of its exports (#32360).
This feature is useful for trimming startup time when a module is expensive to evaluate but rarely used on a given codepath.
deferred.js
console.log("deferred module evaluated");
export const value = 42;main.js
import defer * as deferred from "./deferred.js";
console.log("before access");
console.log(`value: ${deferred.value}`);
console.log("after first access");$ deno run main.js
before access
deferred module evaluated
value: 42
after first accessThe deferred module evaluated line lands between before access and the
property read. Module evaluation is delayed until something actually needs it.
The same semantics are available with import.defer() for dynamic imports. A
common pattern: pre-load both branches of a decision, but only evaluate the one
you actually pick.
png-decoder.js
console.log("PNG decoder evaluated");
export function decode(bytes) {}jpeg-decoder.js
console.log("JPEG decoder evaluated");
export function decode(bytes) {}main.js
const png = await import.defer("./png-decoder.js");
const jpeg = await import.defer("./jpeg-decoder.js");
const format = Deno.args[0];
const decoder = format === "png" ? png : jpeg;
const bytes = Deno.readFileSync(`input.${format}`);
console.log(decoder.decode(bytes));$ deno run -R main.js png
PNG decoder evaluatedBoth modules are fetched and parsed up front, but only the selected one is evaluated.
import defer Works in .ts and .tsx files with no extra setup. deno check
and the LSP both understand the syntax:
main.ts
import defer * as deferred from "./deferred.ts";
const n: number = deferred.value;Learn more about import defer.
TypeScript 6.0.3
The bundled TypeScript compiler is updated to 6.0.3 (#32944). 6.0 is a transition release the TypeScript team uses to land breaking changes and deprecations before the native-port 7.0 ships; see Microsoft’s Announcing TypeScript 6.0 post for the full list.
deno check, deno bundle, the LSP, and deno compile all use the new version
automatically. No flag, no config change.
lib.node included by default
deno check and the LSP now include lib.node in every type-check by default
(#33823). Before 2.8 you had to
add "node" to compilerOptions.lib in deno.json (or sprinkle
/// <reference types="node" /> across files) to get NodeJS.*, Buffer,
process, and the rest of Node’s ambient types to resolve. Now they’re just
there:
node_globals.ts
const buf: Buffer = Buffer.from("hello");
const t: NodeJS.Timeout = setTimeout(() => {}, 0);
console.log(process.versions.node);lib.node is implemented on top of @types/node, and Deno pulls that package
from npm whose major version matches the Node release Deno reports in
process.versions.node. Today that’s the Node 24.x types, in line with the
version returned by:
node_version.ts
console.log(process.versions.node); If you’d rather pin a different version of @types/node (for example because
your project standardizes on Node 22, or because you need a newer patch), just
declare it as a dependency and Deno will use yours instead of the bundled copy:
package.json
{
"devDependencies": {
"@types/node": "^22.10.0"
}
}deno.json
{
"imports": {
"@types/node": "npm:@types/node@^22.10.0"
}
}In practice the default behavior means npm packages with Node-typed APIs (i.e.
nearly all of them) type-check cleanly when imported via npm:, and library
authors writing for both runtimes can rely on NodeJS.Timeout, Buffer, and
friends without telling Deno consumers to configure their TypeScript.
The trade-off is that Node-only globals like process and Buffer are now in
scope at the type level even when you don’t want them, which can quietly
encourage code that won’t run in the browser. The
no-process-global and
no-node-globals lint rules
used to be on by default to catch this; in 2.8 they are off by default but still
available (#33247). Re-enable
them in deno.json if your project targets multiple runtimes:
deno.json
{
"lint": {
"rules": {
"include": ["no-process-global", "no-node-globals"]
}
}
}The runtime globals themselves are unchanged. This is purely a type-level addition. Learn more about including Node types.
Debugging
A major addition to debugging capabilities in 2.8 is that Chrome DevTools can
now inspect Deno’s network traffic. Run your program with --inspect-wait (or
--inspect / --inspect-brk), open chrome://inspect in Chromium, click
Inspect on the Deno target, and the DevTools Network tab now shows every
fetch(), node:http / node:https client request, and WebSocket your
program makes (including server-side WebSockets opened via
Deno.upgradeWebSocket()), with request and response headers, status codes,
bodies, and timing, exactly the way you’d see network traffic in a browser tab.
server.ts
const res = await fetch("https://api.github.com/repos/denoland/deno");
console.log(res.status, (await res.json()).stargazers_count);$ deno run --inspect-wait --allow-net server.ts
Debugger listening on ws://127.0.0.1:9229/...
Visit chrome://inspect to connect to the debugger.
Deno is waiting for debugger to connect.
Under the hood this required implementing the
Network CDP domain
on the inspector side and wiring fetch(), node:http, and WebSocket into it
on the runtime side:
The same events also surface through node:inspector for programmatic clients
and through any other CDP frontend (VS Code’s JavaScript debugger, the
standalone chrome-devtools-frontend, etc.), so tooling that already speaks CDP
against Node can attach to Deno without changes.
Learn more about inspecting network traffic.
CPU profiling
Deno 2.8 ships a built-in CPU profiler that matches Node’s
--cpu-prof flag, plus a few
extras. Start the profiler with --cpu-prof and Deno writes a V8 CPU profile to
disk when the program exits
(#31909):
$ deno run --cpu-prof main.ts
$ ls
CPU.20260519.022823.34721.0.001.cpuprofile main.tsThe .cpuprofile file opens directly in Chrome DevTools (Performance panel,
then Load profile) or any tool that speaks the V8 profile format (eg.
V8’s profview). For times when
you don’t want to load a profile into a UI, two new output formats land
alongside it:
--cpu-prof-flamegraphwrites a self-contained, interactive SVG you can open in any browser, no extra tooling required (#32572).--cpu-prof-mdwrites a human-readable Markdown report with the hottest functions, the call tree, and per-function details.
Combine them in a single run:
$ deno run --cpu-prof --cpu-prof-flamegraph --cpu-prof-md main.ts
$ ls
CPU.20260519.022823.34721.0.001.cpuprofile
CPU.20260519.022823.34721.0.001.svg
CPU.20260519.022823.34721.0.001.mdThe Markdown report is the fastest way to triage a slow run from a terminal:
# CPU Profile
| -------: | ------: | -------: | --------: |
| 187.81ms | 74 | 1000us | 34 |
**Top 10:** `fib` 100.0%
## Hot Functions (Self Time)
| -----: | ------: | -----: | ------: | -------- | --------- |
| 100.0% | 72.00ms | 100.0% | 72.00ms | `fib` | main.ts:1 |The SVG flamegraph is interactive. Click any frame to zoom, hover to see exact timings:
Learn more about CPU profiling in Deno.
Package and workspace management
catalog: protocol
Monorepos that share dependency versions across packages used to require manual
coordination: every member’s package.json had to be updated in lockstep when a
shared dep was bumped. Deno 2.8 adopts pnpm’s
catalog: protocol, letting you declare versions
once in the workspace root and reference them by name from each member
(#32947).
Declare a default catalog in the workspace root:
deno.json
{
"workspace": ["./packages/api", "./packages/web"],
"catalog": {
"hono": "^4.6.0",
"zod": "^3.23.0"
}
}Then reference it from any member with the bare catalog: specifier:
packages/api/package.json
{
"name": "api",
"dependencies": {
"hono": "catalog:",
"zod": "catalog:"
}
}For projects that need multiple catalogs (e.g. one for production, one for build
tooling), use named catalogs under the plural catalogs field:
deno.json
{
"workspace": ["./packages/api"],
"catalogs": {
"runtime": { "hono": "^4.6.0" },
"tools": { "typescript": "^6.0.0" }
}
}packages/api/package.json
{
"dependencies": { "hono": "catalog:runtime" },
"devDependencies": { "typescript": "catalog:tools" }
}catalog: also resolves correctly inside package.json overrides
(#33799) and inside workspaces
declared in the object form
(#33816), so the protocol works
the same way no matter where you reach for it.
Learn more about the catalog: protocol.
Cross-platform npm installs
A lot of popular npm packages ship platform-specific native binaries via
optionalDependencies: esbuild, sharp, rollup, the SWC family, and so on. Deno
reads the os and cpu fields declared on each optional dependency, compares
them to the host platform, and only fetches the binary you can actually run.
Installs stay small, fast, and free of untrusted binaries from platforms you
don’t ship to.
The trade-off shows up when you do want a different platform: building a Linux
ARM64 Docker image from a macOS dev laptop, prepping a CI artifact for Windows,
or pre-populating a cache for a deploy target. The new --os and --arch flags
on deno install tell the resolver to pretend it’s on a different host,
mirroring npm install --os --cpu
(#32785):
$ deno install --os=linux --arch=arm64Supported --os values: aix, android, darwin, freebsd, linux,
openbsd, sunos, win32. Supported --arch values: arm, arm64, ia32,
mips, mipsel, ppc, ppc64, s390, s390x, x64.
Learn more about cross-platform installs.
--prod flag
Production deploys rarely need devDependencies or @types/* packages. Until
now deno install always pulled them in anyway, padding the install size and
adding npm packages you don’t actually ship. The new --prod flag skips both
(#33248):
Drop it into your Dockerfile or CI release step and your production image gets
only the dependencies it needs to run.
Learn more about --prod.
Hoisted node_modules
Deno’s default node_modules layout is isolated: each package gets its own
symlink-resolved tree, so it can only see the dependencies it explicitly
declared. That’s the right default for new projects, but some legacy npm tooling
assumes the flat, hoisted layout that npm install produces, where every
package lives at the top level of node_modules and can require() anything it
finds.
deno.json gets a new nodeModulesLinker field for those cases
(#32788):
deno.json
{
"nodeModulesDir": "manual",
"nodeModulesLinker": "hoisted"
}Valid values are "isolated" (the default) and "hoisted". Use the latter when
porting an existing Node project that relies on the npm-style layout.
Learn more about isolated vs. hoisted node_modules.
.npmrc support
Several gaps in Deno’s .npmrc handling were closed this release.
min-release-age is the headline addition. The feature itself
shipped in 2.6:
Deno refuses to install a package version younger than the configured age, which
catches the vast majority of npm supply-chain attacks before they land in your
tree (malicious versions are typically detected and yanked within a few days of
publishing). In 2.8 you can also configure it from .npmrc, matching the npm
convention so existing tooling keeps working
(#33983):
.npmrc
min-release-age=72hThe remaining improvements unblock common authentication and configuration scenarios with private registries:
certfileandkeyfilefor mutual-TLS authentication (#32655)emailfield on_authentries, used by some legacy on-prem registries (#32616)NPM_CONFIG_REGISTRYcorrectly overrides the registry declared in.npmrc(#32394)
Learn more about .npmrc configuration.
file: and link: dependencies in npm packages
file: and link: specifiers point at a local path on the publisher’s machine
and only make sense during development. Plenty of published npm packages still
ship with one accidentally left in their package.json:
some-published-package/package.json
{
"name": "some-package",
"version": "1.2.3",
"dependencies": {
"lodash": "^4.17.0",
"local-helpers": "file:../local-helpers"
}
}That stray file: entry used to break Deno with a cryptic error during
resolution:
$ deno install
error: Invalid version requirement. Unexpected character.
[email protected] -> local-helpersIn 2.8, file: and link: specifiers are silently skipped while parsing
registry metadata, so packages that carry stray local-path deps install cleanly
(#32876). The actual code those
deps reference is bundled into the published tarball anyway, so nothing’s lost
by ignoring them.
Learn more about file: and link: dependencies.
--package-json flag
Projects migrating from Node often end up with both a package.json and a
deno.json. By default, deno add, deno install, deno remove, and
deno uninstall modify deno.json because that’s where Deno-native projects
keep their dependencies. The new --package-json flag forces those subcommands
to target package.json instead, useful when you want the npm-style manifest to
remain the source of truth for your team’s tooling
(#33199):
$ deno add --package-json express
$ deno install --package-json
$ deno remove --package-json lodashLearn more about --package-json.
Bug fixes
Deno’s package management code is a moving target. Every release brings a lot of small correctness fixes, and 2.8 is no exception: 35 of them landed. A few worth calling out explicitly:
- Peer dependency resolution got two meaningful improvements: a fix for cases where a peer dep ended up installed in multiple conflicting versions and caused hangs (#32358), and memoization of peer-cache hit checks that previously blew up combinatorially on large workspaces (#32609).
- Aliased
package.jsondependencies ("my-name": "npm:foo@1") are now all linked intonode_modules, not just the canonical one (#33068). - Global installs regenerate their lockfile correctly when you pass
--force(#33970). deno update --lockfile-onlyno longer rewrites your config file alongside the lockfile, restoring the contract of the--lockfile-onlyname (#33746).
deno compile updates
deno compile keeps moving toward “point it at a project, get a binary back” as
the default workflow. Two new features cover most of that ground in 2.8, plus a
batch of fixes for binaries that re-launch themselves the way many npm published
CLIs do.
Framework detection
Running deno compile . (or deno compile ./myapp) now auto-detects the web
framework you’re using, runs deno task build to produce build output, and
generates the right entrypoint for it
(#33164). The supported list
covers most of the popular options: Next.js, Astro, Fresh, Remix, SvelteKit,
Nuxt, SolidStart, TanStack Start, and Vite SSR.
$ deno compile .
Compile file:///project/main.ts to file:///project/myapp
Detected Vite SSR project
Running deno task build...
...Entrypoints use import.meta.dirname so paths resolve against the virtual
filesystem inside the compiled binary, which means a Next.js or Astro build
shipped as a single executable now works without a separate runtime wrapper.
Learn more about framework detection.
Other improvements
For projects with large npm dependency trees deno compile used to go silent
for tens of seconds. It now reports progress through each phase
(#33874): an animated progress
bar in an interactive terminal, or per-phase log lines in CI and piped output.
Operations that finish in under 120ms render nothing, so fast paths stay quiet.
A batch of fixes targets compiled npm CLIs that re-launch themselves (e.g.
@google/gemini-cli). child_process.spawn and child_process.fork skip the
Node-to-Deno CLI argument translation when running inside a standalone binary
(#32980), the duplicate exe path
is stripped from argv when a standalone binary relaunches itself
(#33016), and process.argv[1]
now resolves to Deno.execPath() in compiled binaries instead of the entrypoint
URL (#32990). The self-extracting
cache directory also moves to a hidden directory next to the executable
(#32329) so it no longer clutters
the binary’s parent directory.
A few smaller fixes round out the section: --env-file resolves
parent-directory paths again and a missing env file no longer aborts
deno compile (#32686); bundling
CSS treats same-document fragment URLs as external
(#33492); and Deno.bundle
reports a clearer error when called from inside a compiled binary
(#33503).
OpenTelemetry
Deno’s built-in OpenTelemetry integration gets two new exporters and a way to route permission audits straight into your OTel pipeline.
Console exporter
Set OTEL_EXPORTER_OTLP_PROTOCOL=console to print spans, logs, and metrics to
stderr in a human-readable format. No collector required. Handy when you’re
debugging instrumentation locally
(#32717).
$ OTEL_DENO=true OTEL_EXPORTER_OTLP_PROTOCOL=console deno run -A main.ts
SPAN outer span [00000000000000000000000000000001/0000000000000001] Internal 1ms
scope: example-tracer
SPAN inner span [00000000000000000000000000000001/0000000000000002] Internal 0ms
parent: 0000000000000001
scope: example-tracer
key: value
LOG [INFO] "hello from inner"
scope: deno
trace: 00000000000000000000000000000001/0000000000000002gRPC OTLP exporter
The OTLP exporter now speaks gRPC alongside the existing HTTP/protobuf transport. Point it at your collector’s gRPC port and you’re done (#30365).
$ OTEL_DENO=true \
OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com:4317 \
deno run -A main.tsPermission audits as OTel logs
The permission audit log (introduced in 2.5) can now be fed straight into your
OTel exporter. Set DENO_AUDIT_PERMISSIONS=otel and every permission check
becomes an OTel log event correlated with the surrounding span, so you can alert
on unexpected file or network access across your fleet without scraping JSONL
files (#32501).
$ OTEL_DENO=true DENO_AUDIT_PERMISSIONS=otel deno run -A main.tsLearn more about OpenTelemetry in Deno.
Other improvements
- Span attributes copied from HTTP requests onto per-route metrics (#32720)
- Array values supported in OTel attribute maps (#32748)
- Server spans for 4xx responses no longer marked as errors (#32722)
log.iostreamattribute added to console logs (#32723)exception.*attributes added to OTel log records (#32726)
Testing and coverage
Deno’s built-in test runner and coverage tool both pick up a few useful improvements this release.
Sanitizers off by default
The sanitizeOps and sanitizeResources options on Deno.test() now default
to false instead of true
(#33250). These sanitizers fail a
test when async ops or resources outlive it, and in practice they have been a
frequent source of confusing failures, especially for code that uses
setTimeout, node:http, or other APIs whose cleanup is loosely scoped. The
new default matches what most people expect: tests pass when their assertions
pass, and you opt back into the stricter behavior when you actually want it.
You can re-enable sanitizers for a single test:
leak_test.ts
Deno.test(
"leaks a timer",
{ sanitizeOps: true, sanitizeResources: true },
() => {
setTimeout(() => {}, 1000);
},
);…for every test in a file via the new module-level API:
strict_test.ts
Deno.test.sanitizer({ ops: true, resources: true });
Deno.test("leaks a timer", () => {
setTimeout(() => {}, 1000);
});…or globally in deno.json:
{
"test": {
"sanitizeOps": true,
"sanitizeResources": true
}
}Learn more about test sanitizers.
Per-test timeouts
Deno.test() now accepts a timeout option (in milliseconds) that fails a test
if it runs longer than expected, instead of hanging your CI run
(#33815):
slow_test.ts
Deno.test("slow operation", { timeout: 100 }, async () => {
await new Promise((r) => setTimeout(r, 500));
});$ deno test
slow operation ... FAILED (102ms)
ERRORS
slow operation => ./server_test.ts:5:6
error: Test timed out after 100ms.The timeout is per-test, so a slow test no longer drags down everything else in
the suite. Pair it with --parallel to keep total wall-clock time predictable.
Learn more about test timeouts.
Function coverage
deno coverage now reports per-function coverage alongside line and branch
coverage in both the text summary and the HTML report
(#32507). Useful when a file has
high line coverage but most of its API surface is untested:
$ deno test --coverage=cov
$ deno coverage cov
| File | Branch % | Function % | Line % |
| --------- | -------- | ---------- | ------ |
| math.ts | 100.0 | 66.7 | 66.7 |
| All files | 100.0 | 66.7 | 66.7 |In this example two of three exported functions in math.ts are tested: line
and function coverage agree here, but the new column makes it obvious at a
glance which functions are missing tests. Line percentage alone can hide that
when a file has a few large untested functions.
Learn more about function coverage.
Other fixes
deno testdedupes test modules discovered through multiple workspace members so each test only runs once (#32380)deno test --watchrestarts the suite when an--env-filechanges, matching the watch behavior for source edits (#32461)- Coverage line and branch counts are now correct in edge cases involving partially overlapping zero-count ranges (#32312)
deno coveragewarns instead of erroring out when a source file referenced by the profile is missing, so a stale data file doesn’t kill the whole report (#32398)
Web APIs
Deno 2.8 keeps closing the gap with the browser platform, with two new APIs,
expanded Web Crypto coverage, and a long tail of fixes and perf wins across
fetch, streams, TextEncoder/TextDecoder, and friends.
Canvas and geometry primitives
Two browser-platform APIs that server-side code has long been asking for:
OffscreenCanvas (#29357) lands
as a stable global, and the
Geometry Interfaces Module Level 1 spec is
implemented behind --unstable-webgpu
(#27527).
OffscreenCanvas is the same API the browser exposes for off-thread canvas
work: create a canvas without a DOM, get a rendering context, and transfer it
between workers via postMessage. Deno’s implementation supports the
"bitmaprenderer" and "webgpu" contexts (the "2d" and WebGL contexts are
not implemented and getContext returns null for them). A common pattern is
to decode an image, place the resulting ImageBitmap on the canvas, and encode
the canvas back out via convertToBlob:
thumb.ts
const res = await fetch(
"https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=480",
);
const bitmap = await createImageBitmap(await res.blob());
const { width, height } = bitmap;
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("bitmaprenderer")!;
ctx.transferFromImageBitmap(bitmap);
const out = await canvas.convertToBlob({ type: "image/png" });
await Deno.writeFile("out.png", new Uint8Array(await out.arrayBuffer()));$ deno run --allow-net --allow-write=. thumb.tsThat covers headless format conversion, thumbnail generation, and social-card
rendering without a headless browser. Pair it with the "webgpu" context for
off-window GPU-rendered targets.
The geometry interfaces add DOMPoint, DOMRect, DOMQuad, and DOMMatrix
(plus their Readonly variants). These are the same matrix and rectangle types
the browser uses for transforms and hit-testing, and they’re the missing
primitive for code that wants to share geometry math between the browser and
Deno:
geometry.ts
const m = new DOMMatrix()
.translate(100, 50)
.rotate(30)
.scale(2);
const p = new DOMPoint(10, 0).matrixTransform(m);
console.log(p.x, p.y);
const r = new DOMRect(0, 0, 100, 50);
console.log(r.right, r.bottom); $ deno run --unstable-webgpu geometry.tsLearn more about OffscreenCanvas and Geometry Interfaces.
Cloneable and transferable values
The web platform draws a careful line between two ways of moving JavaScript
values across realm and worker boundaries:
serializable objects
can be deep-copied with
structuredClone,
and
transferable objects
can have their ownership moved with zero copy. Deno 2.8 closes long-standing
gaps in both, so you can finally write efficient multi-threaded programs without
serializing state to JSON by hand.
The headline fix is that non-serializable Web types are finally transferable
when correctly listed in the transfer array of structuredClone or
postMessage
(#33491):
transfer.js
const stream = new ReadableStream({
start(c) {
c.enqueue("hello");
c.close();
},
});
const cloned = structuredClone({ stream }, { transfer: [stream] });
worker.postMessage({ stream: cloned.stream }, [cloned.stream]);The full set of types Deno can now transfer is
Headers,
Request,
Response,
ReadableStream,
WritableStream,
and
TransformStream.
The full set of types Deno can now serialize (deep-copy) is
Blob,
File,
CryptoKey,
DOMException,
X509Certificate,
EventLoopDelayHistogram,
and
RecordableHistogram.
Learn more about structuredClone and transferable objects.
Other fixes
crypto.subtle.digestaccepts"SHA3-256","SHA3-384", and"SHA3-512"(#32342).- P-521 (the largest NIST curve) gains first-class support across sign, verify, and ECDH derive (#32602), with EC key export fixed for all formats (#32412, #34087). Importing X25519, X448, and Ed25519 raw keys also validates their length now instead of silently accepting any byte string (#33944).
CacheStorage.keys()andCache.keys()are implemented (#33275), filling in the iteration entry points on the Cache API.AbortSignal.any()no longer leaks memory when wrapping long-lived signals (#32916).subtle.importKeyno longer panics on a wrong algorithm name (#32410); it throws as specified.getRandomValuesthrowsTypeMismatchErrorfor non-TypedArrayinputs (#33470).fetchretries on stale pooled HTTP/1.1 connections (#32566) and stops mutating the caller’s options inDeno.createHttpClient(#33497). The response resource is closed cleanly when an abort races op completion (#33928), and NodeReadablerequest bodies stream through a byteReadableStream(#33432).Deno.listenDatagramdefaults its hostname to0.0.0.0(#33496), matchingDeno.listen.- HTTP upgrade handlers can now reject with non-101 status codes
(#32615), an empty
Hostheader is treated as missing (#33234), and a WebSocket H2 stream reset no longer panics (#33982). MessageEventports convert via the WebIDL sequence iteration protocol (#33652), andsourceis now retained fromMessageEventInit(#33500).- A few Node-aligned error code touch-ups:
ERR_MISSING_ARGSonURL.revokeObjectURL()(#33471),ERR_ILLEGAL_CONSTRUCTOR(#33535) andERR_INVALID_THIS(#33467) on the correspondingTypeErrors, andAbortSignal.timeout’s error message matches Node’s (#33460). console.logsupports the%jJSON format specifier (#32684), andconsole.dirxmlnow routes through the log printer (#33443).QuotaExceededErroris upgraded to aDOMException-derived interface (#32244),removeEventListenerhandles anulloptions argument (#32605), andEvent.returnValuerespects thecancelableandpassiveflags (#33651).ReadableStreamBYOBRequest.viewis narrowed toUint8Array(#33477), a late write racing withTransformStream.cancelno longer hangs (#33478), and aWebTransportdatagram-overflow infinite loop is fixed (#33075).GPUQueue.writeBuffer()accepts plainArrayBufferdata sources (#33152).- WebSocket response headers handle non-ASCII bytes correctly (#32594).
Task runner
deno task got a small quality-of-life improvement that matters once you start
running tasks in parallel: every output line is now prefixed with the task name
that produced it, so interleaved stdout from concurrent tasks no longer reads as
one tangled stream (#33805).
Given a config that fans out to two builds:
deno.json
{
"tasks": {
"client": "echo building client && sleep 1 && echo client ready",
"server": "echo building server && sleep 1 && echo server ready",
"dev": { "dependencies": ["client", "server"] }
}
}…running deno task dev interleaves the two scripts cleanly:
$ deno task dev
Task client echo building client && sleep 1 && echo client ready
[client] building client
Task server echo building server && sleep 1 && echo server ready
[server] building server
[client] client ready
[server] server readydeno task fanning out to three child tasks in parallel, with each output line prefixed and color-coded by task name.The prefixes are color-coded per task and stay attached even when a task forks
subprocesses, so a parallel build + test + lint workflow stays legible
without piping everything through a separate multiplexer.
The task shell itself also picks up a couple of POSIX features: set -e /
set -o errexit (and set +e to turn it back off) aborts the surrounding
sequential list on the first non-zero exit, and the POSIX null command : is
now a builtin. Both make it easier to port shell scripts into tasks blocks
without resorting to a separate bash -c wrapper.
The rest of the changes are workspace fixes that make --filter and recursive
task discovery match user expectations:
--filternow matches a workspace member by directory name as well as package name (#33499), and falls back to the directory name only when an explicit workspace root check passes (#33540)- Recursive task name completion works inside workspaces, so shell tab-completion sees tasks declared in any member (#32422)
- Backticks in arguments forwarded to a task are escaped correctly instead of being re-evaluated by the task shell (#34151)
Learn more about task dependencies and parallel output.
deno upgrade changes
Two improvements to deno upgrade worth flagging: it’s a lot smaller over the
wire now, and you can use it to grab a Deno build straight from a PR.
Delta updates
deno upgrade now downloads a binary diff between your current version and the
target instead of the full release archive, when one is available
(#33274). A typical patch-level
upgrade goes from a roughly 48 MB download to 3-6 MB, an 87-93% bandwidth
reduction. Useful for everyone, but especially for CI runners and ephemeral
environments that pull Deno on every job.
The implementation chains up to three
bsdiff patches to reach the target (e.g.
2.7.14 to 2.8.0 to 2.8.1 to 2.8.2). Every patch and every intermediate binary is
SHA-256-checked against the published checksums, so a bad delta can’t quietly
corrupt your install. If any step fails (missing delta asset, checksum mismatch,
patch error), Deno transparently falls back to a full download. The --no-delta
flag forces the full path if you ever want to bypass deltas entirely.
Learn more about delta updates.
Install from a PR
deno upgrade pr <number> downloads the binary that CI built for a given pull
request and replaces your local deno with it, so you can try a fix or new
feature without building from source
(#33252). It uses the
gh CLI under the hood, so you need it installed and
authenticated:
$ deno upgrade pr 34227
$ deno upgrade --output ./deno-pr pr 34227
$ deno upgrade --dry-run pr 34227Under the hood deno upgrade queries gh pr view, walks the PR’s CI runs to
find the right artifact for your platform (preferring release over debug),
downloads it, sanity-checks it with deno -V, then swaps it in. Faster than a
git clone && cargo build round-trip when you just want to verify a fix.
Learn more about installing a build from a PR.
Module loader hooks
Technically a piece of Node.js API compatibility, but big enough to deserve its
own section: Deno 2.8 implements Node’s
module.registerHooks()
API (#34081). Loader hooks let
you customize module loading at runtime: intercept resolution, transform source,
redirect specifiers, mock dependencies for tests, or add instrumentation, all
without rebuilding Deno or running a separate build step.
As a concrete example, here’s a 14-line loader that teaches Deno how to import
a .css file as the stylesheet text:
css-loader.ts
import module from "node:module";
module.registerHooks({
load(url, context, nextLoad) {
if (!url.endsWith(".css")) {
return nextLoad(url, context);
}
const css = Deno.readTextFileSync(new URL(url));
return {
format: "module",
source: `export default ${JSON.stringify(css)};`,
shortCircuit: true,
};
},
});button.css
.button {
padding: 8px 16px;
border-radius: 4px;
background: #66c2ff;
}Pre-load the hooks with --import so they register before user code runs:
main.ts
import buttonCss from "./button.css";
console.log(buttonCss);$ deno run --allow-read --import ./css-loader.ts main.ts
.button {
padding: 8px 16px;
border-radius: 4px;
background:
}You could also get a CSS file as text via the standard
import attributes proposal
(import css from "./button.css" with { type: "text" }) that
we shipped back in Deno v2.4
. The point of the example above isn’t .css specifically; it’s showing the
mechanics of writing a custom loader for arbitrary file types or
transformations.
Loader hooks also work inside binaries produced by deno compile, which makes
them useful for distributing self-contained CLIs that ship with custom
resolution baked in. Here’s a virtual-module loader that exposes a
virtual:greeting specifier from inside the compiled binary:
greeter.ts
import module from "node:module";
module.registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier !== "virtual:greeting") {
return nextResolve(specifier, context);
}
return { url: "virtual:greeting", shortCircuit: true };
},
load(url, context, nextLoad) {
if (url !== "virtual:greeting") return nextLoad(url, context);
return {
format: "module",
source: `export default "Hello from a virtual module!";`,
shortCircuit: true,
};
},
});
const { default: greeting } = await import("virtual:greeting");
console.log(greeting);$ deno compile -A --output greeter greeter.ts
$ ./greeter
Hello from a virtual module!We chose not to implement the
module.register()
API, since Node has deprecated it and plans to remove it.
Learn more about module loader hooks in Deno.
setTimeout and setInterval
A change we’ve been planning for a long time
finally lands in 2.8: setTimeout and setInterval now return Node’s Timeout
object instead of an opaque number, matching node:timers behavior at the
global scope (#33249).
This is technically a breaking change, but in practice it should affect very few programs. The common case keeps working untouched:
timer.ts
const t = setTimeout(() => console.log("hi"), 1000);
clearTimeout(t);The only code that breaks is code that relied on the return value being a
number, e.g. storing it in a number-typed field, doing arithmetic on it, or
runtime-checking its type:
timer_id.ts
const id: number = setTimeout(fn, 0);
const id: NodeJS.Timeout = setTimeout(fn, 0);timer_check.ts
if (typeof timer === "number") {
clearTimeout(timer);
}
clearTimeout(timer);Why is it worth even this small amount of churn? Three reasons:
- Performance. The compatibility shim we used to wrap web-style
setTimeoutaroundnode:timerssat on a hot event-loop path. Removing it cuts overhead on every timer call. - Tech debt. Maintaining two parallel timer implementations (web and Node) plus a global proxy that converted between them was a constant source of subtle bugs.
- Simpler mental model. There is now one global
setTimeoutin Deno, it behaves the waynode:timersdoes, and the sameTimeoutobject flows through both styles of code. The global proxy that used to intercept these calls is gone.
The NodeJS.Timeout type used above resolves out of the box, thanks to
lib.node now being included by default.
Miscellaneous
A few smaller changes worth flagging that don’t fit neatly under any of the sections above:
- The
NODE_EXTRA_CA_CERTSenvironment variable is now honored at the root certificate store level (#33148), so extra CA certificates apply to every TLS code path:fetch(),Deno.connectTls(),node:https, andnode:tls. Useful behind corporate MITM proxies or when you need to trust an internal CA across a whole script. - Linux release builds run again on systems with glibc older than 2.27 (#33259), restoring compatibility with a long tail of older distros and CI images.
deno compileconfiguration growsincludeandexcludefields, so you can declare bundled data files indeno.jsoninstead of repeating--includeflags on every invocation (#33024, docs).- Several additional Unix-style signals (
SIGUSR1,SIGUSR2, and others) are now usable on Windows for Node compatibility (#32689). deno xaccepts a--package/-pflag for the case where the package name differs from the binary name (e.g. runningtscfromnpm:typescript) (#32855).deno evalauto-detects CommonJS vs ES module syntax in the input snippet, so quick one-liners work without picking a flag (#32472).Deno.upgradeWebSocketnow wires up cleanly tonode:httpupgrade events, so Node-style servers can hand off WebSocket connections to Deno’s native implementation (#33342).Deno.watchFspaths are normalized so events no longer carry leading./segments that broke string-equality checks against the watched paths (#33490).- FFI calls hold onto their argument backing stores for the duration of the call, preventing a use-after-free when async FFI work outlives the caller’s stack frame (#32775).
- The event loop now wakes correctly when V8 posts foreground tasks from background threads, fixing a class of subtle hangs around async module evaluation (#32450).
deno_graphis bumped to 0.108.2 for proper handling of WebAssembly multi-value return types (#34070).deno doclearns to render npm packages (#32435), with follow-up fixes for npm entrypoints that ship without types (#34147). The HTML output also picks up Prism highlighting for JSX/TSX code blocks (#33255) and cleaner operator rendering in dark mode (#33267).--watchrestarts now sendSIGTERMand dispatchunload/process.exitbefore tearing the process down, so cleanup handlers actually run (#32564, #32664). The grace period before the hard kill drops from 5 seconds to 500 milliseconds (#33099), and--watch-excludepatterns are now respected for every file change event, not just the initial scan (#33854).- Text imports (
with { type: "text" }) are stable and no longer require--unstable-raw-imports(#34238, introduced in 2.4). Bytes imports (with { type: "bytes" }) remain behind the flag. Learn more about import attributes. deno publishno longer panics during provenance generation when run on CI providers other than GitHub (#33802).- Vite’s
import.meta.hotreferences now type-check correctly (#32127).
V8 14.9
Deno 2.8 upgrades the V8 engine from 14.6 to 14.9 (#34226).
Acknowledgments
We couldn’t build Deno without the help of our community! Whether by answering questions in our community Discord server or reporting bugs, we are incredibly grateful for your support. In particular, we’d like to thank the following people for their contributions to Deno 2.8: Amol Yadav, Ashwin Naren, Avocet, BitToby, Dan Dascalescu, Daniel Osvaldo Rahmanto, Daniil Sivak, David Sherret, em, Felipe Cardozo, Fibi, Hajime-san, Hunnyboy1217, Janosh Riebesell, Jean Ibarz, Jimmy, John L. Carveth, Josh Fleming, kaju, Kenta Moriuchi, Kit Dallege, KnorpelSenf, KT, Kyle Kelley, Lach, Leo Zaki, lif, Luca Barbato, Luna, Marvin Hagemeister, Michael Horstmann, Nayeem Rahman, Nik B, Olivér Falvai, Pietro Marchini, r3wretrhy, Rano | Ranadeep, Rohan Santhosh Kumar, RoomWithOutRoof, Shivam Tiwari, tmimmanuel, Varun Chawla, and web-dev0521.
Would you like to join the ranks of Deno contributors? Check out our contribution docs here, and we’ll see you on the list next time.
Believe it or not, the changes listed above still don’t tell you everything that got better in 2.8. You can view the full list of pull requests merged in Deno 2.8 on GitHub.
That’s all for 2.8, thanks for reading and see you in the next release.