Blog

Bun v1.4.1

Bun v1.4.1 fixes 202 issues, addressing 236 πŸ‘.

To install Bun

curl

curl -fsSL https://bun.sh/install | bash

To upgrade Bun

bun upgrade

New in the runtime#

Reduced idle memory usage#

We've made JavaScriptCore delete JIT-generated code after an extended idle period, reducing memory usage for long-running processes.

RSS after 60 seconds of load followed by 3 minutes idle, on Linux x64:

AppBun v1.4.1Bun v1.4.0Bun v1.3.14Node.js v26
Next.js SSR142 MB222 MB1,303 MB195 MB
vite dev111 MB142 MB292 MB115 MB
Express53 MB65 MB76 MB83 MB
Fastify55 MB65 MB78 MB89 MB
Elysia44 MB46 MB69 MB96 MB
Hono34 MB35 MB53 MB92 MB

Bun.serve supports HTTP/2#

Bun.serve now supports HTTP/2 on the same port as HTTP/1.1. Both protocols use the same routes and fetch handler. req.url and req.body behave identically on each.

Bun.serve({
  tls: { key, cert },
  http2: true,
  fetch(req) {
    return new Response("hi");
  },
});

Over TLS the protocol is negotiated with ALPN. On cleartext connections, a client that sends the HTTP/2 preface is served HTTP/2. Set http1: false to refuse HTTP/1.x clients. WebSockets and response trailers are not yet supported over HTTP/2.

Bar chart: HTTP/2 over TLS requests per second on Linux x64, Bun.serve() vs node:http2 in Bun vs node:http2 in Node.js v26. Hello World GET: 291,512 vs 55,619 vs 42,733 (6.8x faster). POST 4 KB body: 136,077 vs 45,366 vs 30,112 (4.5x). 2 KB URL: 113,648 vs 43,168 vs 31,249 (3.6x). 64 KB response: 17,357 vs 13,771 vs 12,979 (1.3x). Streaming 256 KB in, 32 KB out: 4,570 vs 4,563 vs 2,164 (2.1x). Bun v1.4.1 vs Node.js v26.3, 64 cores, loopback, h2load -c 64 -m 16 -t 8 -n 100000.

Bun.write(path, response) streams to disk#

Bun.write() now streams a Response, Request, or ReadableStream body into the file instead of reading it into memory first. Writing a 128 MiB download previously added 161 MB to peak RSS. It now adds 13 MB.

await Bun.write("./big.tar.gz", await fetch(url)); // => bytes written
await Bun.write("./out.txt", readableStream); // was "[object ReadableStream]"

fetch() and S3 downloads now pause when 256 KiB of the body is unread. A Response that is discarded without being read closes its connection.

WebSocket supports pause() and resume()#

Bun's WebSocket client now has pause() and resume() methods. They stop and restart reads from the underlying TCP socket.

const socket = new WebSocket("wss://example.com/feed");

socket.addEventListener("message", (event) => {
  if (!file.write(event.data)) {
    // the file's buffer is full: stop reading until it drains
    socket.pause();
    file.once("drain", () => socket.resume());
  }
});

The WHATWG WebSocket API cannot stop receiving messages without closing the connection. Messages that arrive faster than they are processed accumulate in memory. While a socket is paused, Bun does not read from it and the peer receives TCP backpressure.

socket.pause(); // => true
socket.isPaused; // => true
socket.resume(); // => true
socket.isPaused; // => false

Messages that were already decoded before the pause can still arrive. This is a Bun extension and is not available in browsers.

socket.bufferedAmount now reports how many bytes are still queued to send. It was previously always 0. The ws package's pause(), resume(), and bufferedAmount work too.

node:crypto supports crypto.argon2 and crypto.argon2Sync#

crypto.argon2() and crypto.argon2Sync() are now implemented. Previously, both were defined but threw ERR_CRYPTO_ARGON2_NOT_SUPPORTED when called. Feature detection therefore reported support that did not exist.

crypto.argon2Sync("argon2id", {
  message: "pw",
  nonce: "saltsalt",
  parallelism: 1,
  tagLength: 32,
  memory: 8,
  passes: 1,
});
// => <Buffer ...> (32 bytes)
  • argon2d, argon2i, and argon2id are supported.
  • crypto.argon2() runs on the thread pool with a callback.
  • Output matches Node byte for byte.

Up to 9x faster Buffer reads and writes#

buf.readInt32LE(), buf.writeDoubleLE(), and the rest of the Buffer read*/write* methods are inlined by the JIT into a bounds-checked load or store.

buf.writeFloatLE(1.5, 0); // 2.85ns -> 0.31ns (9.2x faster)
buf.writeUInt8(255, 0); // 2.24ns -> 0.31ns (7.2x faster)
buf.readUInt32BE(0); // 0.76ns -> 0.42ns (1.8x faster)

Measured on macOS arm64 against Bun v1.4.0. The write* methods sped up the most.

readIntLE(offset, byteLength) and its variable-width siblings are inlined too when byteLength is a constant 1, 2, or 4.

Faster AsyncLocalStorage#

AsyncLocalStorage.run() is about 2x faster. An active store no longer costs an extra allocation on every await, .then(), or .finally(). An await inside run() is now as cheap as one outside it.

const als = new AsyncLocalStorage();

als.run({ requestId }, async () => {
  await next(); // no per-await allocation for the store
  als.getStore(); // { requestId }
});
ns/opNode 26Bun, beforeBun v1.4.1
als.run(v, fn)41028.815.9
3x nested run()283333857.8
await under a store36.844.134.5
Promise.all of N .then()435345281

enterWith() called inside a timer, I/O callback, or request handler no longer leaks its store into the next unrelated callback. This matches Node.js.

Faster Bun.inspect, console.log, and util.inspect on large values#

Printing an object took time quadratic in its number of properties. It is now linear. expect() failure messages use the same code and are also faster.

Bun.inspect(objectWith16000Keys); // 140 ms -> 3.2 ms (43x faster)

util.inspect now stops walking an array or typed array once it reaches maxArrayLength, instead of visiting every element and discarding the rest. Output is unchanged. REPL tab completion on big arrays gets the same speedup.

util.inspect(new Array(2e6).fill(7), { maxArrayLength: 3 }); // 2090 ms -> 42 ms

require() is faster for built-in Node.js modules#

node:fs, node:assert, node:util, node:vm, node:http and a few others load their internals on first use instead of at require() time.

ModuleBun v1.4.1Bun v1.4.0
node:assert0.64 ms6.22 ms
node:https4.77 ms8.14 ms
node:http4.72 ms7.44 ms
node:vm0.55 ms2.57 ms
node:fs0.87 ms2.75 ms
node:util1.02 ms2.32 ms
node:tty3.25 ms4.35 ms
node:worker_threads1.42 ms2.43 ms
node:http26.87 ms7.58 ms
node:child_process1.15 ms1.59 ms

Faster first HTTPS request#

The first TLS connection in a process starts up to 3x faster.

Time from process start to the first fetch("https://…") response, on Linux x64:

Root certificatesBun v1.4.1Bun v1.4.0
Bundled (default)10.3 ms17.6 ms
System (--use-system-ca)15.3 ms46.3 ms

Bun now embeds its root certificates as DER and only parses one when a certificate chain needs it.

An expired certificate in a trust store no longer hides a valid certificate for the same issuer. BoringSSL is updated to current upstream.

fetch() reuses unix socket connections#

fetch() over a unix socket keeps connections alive and reuses them, like it already does for TCP.

for (let i = 0; i < 3; i++) {
  await fetch("http://localhost/x", { unix: "/tmp/app.sock" });
}
// before: 3 connections. now: 1
  • fetch(url, { unix, tls: { ca } }) uses the custom ca.
  • A relative unix path no longer connects to the wrong socket after process.chdir().

localhost and *.localhost resolve to loopback everywhere#

fetch(), WebSocket, Bun.connect(), and bun install now resolve localhost and any *.localhost name to ::1 and 127.0.0.1 without querying the system resolver. This fixes connecting to a Bun.serve() bound to "localhost" inside a Docker container that only has IPv4.

await fetch("http://app.localhost:3000"); // works on macOS, Windows, and Linux

node:net and node:dns are unchanged.

fetch() verifies TLS against the URL, not the Host header#

If tls.servername was not set, fetch() used a custom Host header as the TLS server name and checked the certificate against it - matching what node:https does. Bun now uses the URL's hostname, as Node's fetch and curl do. The Host header is still sent, but it no longer affects TLS.

The previous behavior was an insecure default. Applications that pass a Host header from user input to fetch(), as many proxies do, should upgrade.

// no longer fails with ERR_TLS_CERT_ALTNAME_INVALID
await fetch("https://a.example/", {
  headers: { Host: "b.example" },
});

To connect to an IP address and verify the certificate against a different name, set tls.servername:

await fetch("https://10.0.0.5/", {
  tls: { servername: "internal.example" },
});

binaryType = "blob" on server WebSockets#

ServerWebSocket and the built-in ws package now accept "blob", like the native WebSocket client.

ws.binaryType = "blob"; // binary messages arrive as Blob

Previously, a ws server socket in "arraybuffer" mode emitted a Uint8Array. It now emits an ArrayBuffer to match npm ws.

--env-file reads pipes, FIFOs and /dev/stdin#

An explicit --env-file can now read from process substitution, a named pipe, or stdin, as in Node. Secret managers such as 1Password can provide environment variables this way without writing them to disk.

bun --env-file=<(echo A=1) -e 'console.log(process.env.A)' # => 1
echo A=1 | bun --env-file=/dev/stdin app.ts

Reading from a FIFO blocks until a writer opens it. The default .env* lookup still skips non-regular files.

--no-ffi-cc disables cc() from bun:ffi#

This prevents untrusted JavaScript from compiling and loading C code at runtime.

bun --no-ffi-cc ./script.ts
# cc() throws ERR_FFI_CC_DISABLED

--no-addons disables cc() too. Workers inherit both flags and cannot re-enable them.

Import attributes are typed on TypeScript 7.1#

With TypeScript 7.1 or later, bun-types types an import by its with { type } attribute.

import html from "./index.html" with { type: "text" }; // string, not HTMLBundle
import db from "./my.db" with { type: "sqlite" }; // Database

Covers text, file, md, markdown, toml, yaml, jsonc, json5, xml, sqlite, and html.

Older TypeScript versions keep the extension-based types.

Upgraded JavaScriptCore#

Bun's JavaScriptCore includes about 400 upstream WebKit commits. They include fixes for JIT miscompiles and for crashes on deeply nested destructuring patterns.

await import("./flaky.ts").catch(() => {});
await import("./flaky.ts"); // retries the load instead of rejecting with the cached error
  • Promise.try follows the updated spec.
  • WebAssembly.Module.imports() and exports() descriptors no longer include the non-standard type field.

New in bun install#

Self-contained node_modules for workspace packages#

Workspace packages listed under "selfContained" in the "workspaces" object of package.json get a self-contained node_modules. This is for tools that expect a specific node_modules layout, such as Electron.

package.json

{
  "workspaces": {
    "packages": ["apps/*"],
    "selfContained": ["apps/desktop"]
  }
}

Yarn's "installConfig": { "hoistingLimits": "workspaces" } in the workspace's own package.json works too.

bun install --offline#

bun install --offline makes no network requests. Every package must already be in Bun's cache. It is intended for CI jobs that restore a cache and for machines without network access.

bun install --offline

To make it the default, set it in bunfig.toml:

bunfig.toml

[install]
offline = true

A package missing from the cache is an error that names it:

error: --offline: no cached manifest for "left-pad"
(run once online, or use --prefer-offline)

bun install --prefer-offline#

By default, bun install checks for newer versions by re-fetching a package's metadata once the cached copy expires. --prefer-offline skips that check and uses the cached copy regardless of age. Packages that are not in the cache are still downloaded.

bun install --prefer-offline

To make it the default, set it in bunfig.toml:

bunfig.toml

[install]
prefer = "offline"

New in bun build#

Smaller bundles for libraries that use export * as#

Libraries like zod and Effect group their exports with export * as. When a program called z.object(), Bun 1.4.0 kept every export of that group. It built a namespace object with a getter for each one. Bun 1.4.1 compiles z.object to a direct reference to the object function, and unused exports are tree-shaken.

out.js

var User = exports_external.object({ a: exports_external.string() });
var User = object({ a: string2() });

Each program below calls two or three functions from one library, built with bun build --minify:

PackageBun 1.4.0Bun 1.4.1Change
fp-ts 2.1621.8 KB3.2 KBβˆ’85%
zod 4.5375.3 KB77.3 KBβˆ’79%
ox 1.715.4 KB4.7 KBβˆ’70%
effect 3.22369.1 KB163.6 KBβˆ’56%
@sinclair/typebox 0.34107.1 KB53.3 KBβˆ’50%

In the zod bundle, Bun 1.4.0 kept this object with 252 entries:

Bun 1.4.0 output

var exports_external = {};
__export(exports_external, {
  $brand: () => $brand,
  NEVER: () => NEVER,
  ZodAny: () => ZodAny,
  // ...249 more
});

Every entry kept its export in the bundle, including ZodDiscriminatedUnion, ZodRecord, ZodTemplateLiteral, the JSON Schema generator, and 62 locale files. Bun 1.4.1 removes all of them.

The namespace objects that are still generated accept assignment, which a real module namespace does not. To make them getter-only so that assigning throws, set deprecatedNamespaceObjectSetters: false or pass --no-deprecated-namespace-object-setters. This will be the default in a future release.

Tree-shaking through dynamic import()#

bun build now removes the exports of a dynamically imported module that the importing code never reads.

main.ts

// is.ts exports isNumber, isOdd, and isEven
const { isOdd } = await import("./is");
console.log(isOdd(3));

With bun build ./main.ts --splitting --outdir dist, is.ts becomes its own chunk:

dist/is-1qza7reb.js

function isNumber(n) {
  return typeof n === "number";
}
function isOdd(n) {
  return isNumber(n) && n % 2 === 1;
}
function isEven(n) {
  return !isOdd(n);
}
export {
  isEven,
  isNumber,
  isOdd
};

If the namespace escapes (passed to a function, spread, Object.keys(ns), computed access), every export is kept.

This also works without --splitting when every use of the import() result reads a named export. const { z } = await import("zod") bundles the same code as import { z } from "zod".

bun build ./main.ts --minify --outfile=out.js
# zod 4.5      377.9 KB -> 78.2 KB
# effect 3.22  248.1 KB -> 125.7 KB

The same applies to require() of an ES module. A CommonJS module keeps every export.

Fewer, smaller chunks with --splitting#

bun build --splitting now writes fewer files. Code that an entry shares with the routes it loads through import() stays in the entry's chunk. Previously it was placed in a separate chunk, which required one more request.

main.ts

import { format } from "./format";
console.log(format(1));

document.body.onclick = async () => {
  // settings.ts also imports ./format
  const { settings } = await import("./settings");
  settings();
};

This previously produced 3 files. It now produces 2.

BeforeNow
main.jsmain.js (includes format.ts)
settings-mn3j380d.jssettings-mn3j380d.js
chunk-2fk8vd1p.js (format.ts)

In a test app with one entry and 40 lazy routes:

BeforeAfter
Output files219151
Output size124 KB75 KB
Modules loaded at startup702

Two related bugs are also fixed:

  • An entry no longer imports chunks it uses nothing from.
  • Shared chunks run in the same order as the unbundled code.

--min-chunk-size#

--min-chunk-size folds small chunks into a chunk that more entries load.

bun build ./index.tsx --splitting --min-chunk-size=16384 --outdir out
await Bun.build({
  entrypoints: ["./index.tsx"],
  splitting: true,
  minChunkSize: 16 * 1024,
});

Only chunks with no top-level side effects are folded. Lazily loaded code stays lazy, and side effects do not run earlier.

Each entry gains at most about 1.5% unused code, so a large value is safe. The default is 0, which turns it off.

bun build ./index.html --production --splittingwithout--min-chunk-size=16384
Medusa admin dashboard, JS files349245
Requests per route navigation, median / max13 / 758 / 30
50-route React SPA, JS files242123
Requests per route navigation5415

Module preloading for code-split browser builds#

With --splitting and --target browser, Bun adds <link rel="modulepreload"> for the chunks your code needs, so the browser fetches them in parallel.

bun build ./index.html --splitting --outdir dist

HTML entrypoints get a <link rel="modulepreload"> for every chunk their script imports:

dist/index.html

<head>
  <script type="module" crossorigin src="./index-9tft256y.js"></script>
  <link rel="modulepreload" crossorigin href="./index-60vtcrm3.js">
</head>

A lazy import() now also preloads the chunks that its target imports. In the example below, settings-msmf5hdm.js imports two chunks. __preload adds a <link rel="modulepreload"> for each of them, so the browser fetches them in parallel with settings-msmf5hdm.js. Previously, the browser found those two chunks only after settings-msmf5hdm.js had loaded and been parsed. That took a second round trip.

dist/index-9tft256y.js

const { settings } = await import("./settings-msmf5hdm.js");
const { settings } = await (__preload("msmf5hdm"), import("./settings-msmf5hdm.js"));
// preloads index-60vtcrm3.js and index-0fxtmqn2.js

An import() inside a lazily loaded chunk gets the same __preload. Previously, the browser discovered each chunk only after the previous one loaded. This added one round trip per level.

This is on by default. To turn it off, set modulePreload: false in Bun.build or pass --no-module-preload.

Smarter CommonJS to ESM conversion for default imports#

When you import pkg from "pkg" and pkg is CommonJS, bun build now tries to compile pkg.foo to a plain top-level variable instead of a property read on a wrapper object. It does this when it can see every export the module assigns, which covers packages like react, react-dom, and scheduler.

Previously, this was only supported for import * as pkg from 'pkg' or named imports, now it also is supported on default imports - and several bugs fixed along the way.

in.js

import React from "react";
const el = React.createElement("div");

out.js

var import_react = __toESM(require_react());
var el = import_react.default.createElement("div");
var el = $createElement("div");
# import React + React.createElement("div"), react@19, --production --minify
8,206 -> 1,392 bytes

The same conversion now also applies through module.exports = require("./impl") redirect files such as react-dom/index.js. Bun falls back to the wrapper when it cannot prove the conversion is safe: the module assigns module.exports dynamically, assigns both module.exports and exports.foo, reads module for anything other than module.exports, or is external.

require() is a chunk boundary with --splitting --target bun#

A require() of an ES module now gets its own chunk, as with import(). The call remains synchronous. A require() inside a function that never runs no longer loads that module at startup.

const { tool } = require("./tool.ts");
// bundled as: import.meta.require("./chunk-a1b2c3.js")

A --compile --bytecode benchmark had 400 lazily required modules, and 8 of them were used at startup. Startup time went from 23.2 ms to 11.2 ms and memory use from 34.6 MB to 6.9 MB.

Pass splitRequire: false or --no-split-require to keep the old inlined output.

Smaller code-split bundles#

bun build --splitting --minify assigns each binding shared between chunks a single bundle-wide name. Chunks no longer rename imports and exports.

// before
import { Qc as ur } from "./chunk-a.js";
// after
import { a } from "./chunk-a.js";

On a 100-route app with 600 shared modules, minified output went from 421 KB to 340 KB (19% smaller, and 18.7% smaller gzipped).

Nested classes and functions keep their names in bun build#

bun build no longer renames a class or function in a nested scope from a name like Abc to Abc2 when a top-level binding in the bundle shares its name. .name and constructor.name now match unbundled code, Node.js, and Rollup.

const factory = () => {
  class Model {}
  return Model;
};
const Model = factory();
Model.name; // was "Model2", now "Model"

A nested binding is still renamed when its scope references the outer name. --minify output is unaffected.

bun build is also faster on large ESM inputs. Bundling three.js Γ—100 went from 1.83s in v1.4.0 to 1.12s on Linux x64.

New in bun build --compile#

Compiled executables start faster#

We added fast paths to Bun's module loader for executables built with bun build --compile. Claude Code is compiled with Bun. Its time to the input box dropped 20%, from 397 ms to 318 ms.

Claude Code time to input box is now 20% faster: 397 ms on version 2.1.250, 318 ms on version 2.1.252. Linux x64, median of 25 interleaved launches.

Smaller bytecode#

Previously, JavaScriptCore's serialized bytecode was about 9x larger than the source code it parsed from. Packing structs, deduplicating data, and moving data reduced the multiple from 9x to 3x. This made Claude Code's install 45% smaller, from 376 MB to 207 MB.

Claude Code install size on macOS arm64 is 45% smaller: 376 MB to 207 MB. The bytecode went from 258 MB to 88 MB, and the ratio of bytecode to JS source went from 9 to 1 down to 2.6 to 1.

--bytecode-depth is a new option that limits how many levels of nested functions get bytecode ahead of time. Functions past the limit compile on their first call.

bun build --compile --bytecode --bytecode-depth=1 ./app.ts --outfile app

Cross-compile with --bytecode#

Cross-compilation with --bytecode is now supported. Previously, this did not work when cross-compiling to or from Windows x64 on macOS or Linux.

bun build --compile --bytecode --target=bun-windows-x64 ./app.ts

The bytecode cache format is now the same on every platform, so two builds of the same input produce byte-identical executables.

Smaller text imports in bun build --compile#

Text imports in a compiled executable are now embedded once in pre-encoded form. JavaScript reads them without parsing or copying.

Previously, a 3 MB ASCII text import added 6 MB to a --bytecode binary and 12.6 MB of memory. It now adds 3 MB to the binary and about 1 MB of memory.

import readme from "./README.md" with { type: "text" };
// no longer parsed as a module or copied onto the heap at startup
  • Fixed: import addon from "./x.node" threw __require is not defined in ESM bundles.

New in bun test#

bun test --isolate no longer leaks between files#

Test files that call mock(), spyOn(), mock.module(), or Bun.plugin() no longer keep their module graph alive after they finish. Memory under --isolate now stays flat as files run.

bun test --isolate   # 80 files with mocks: 1591 MB -> 215 MB, 1.95s -> 1.46s
  • process.env.TZ, proxy, and TLS settings no longer carry into later test files.
  • A file that leaves monitorEventLoopDelay() enabled no longer crashes the run.

Other improvements#

  • Improved: the release bun binary is about 1 MB smaller.
  • Improved: bun -e uses about 4% less memory and no longer spawns an idle allocator thread.
  • Improved: paused sockets on macOS no longer wake the event loop for every incoming packet.
  • Improved: faster stream consumers, ReadableStream.from(), and async-iterable Response bodies.
  • Improved: WebSocket clients with permessage-deflate use about 12 KB less memory per connection.
  • Improved: Blob.text(), Response.text(), and TextDecoder.decode() throw ERR_MEMORY_ALLOCATION_FAILED when out of memory, like Node's Blob.text() and Response.text().
  • Improved: bun dedupe, bun prune --production, and bun pm licenses print clearer output without false warnings.
  • Improved: bun build --compile executables ignore NODE_COMPILE_CACHE from the environment.
  • Improved: CSS parse errors print the offending token as written, e.g. @c or url(x) instead of c or x.
  • Improved: hardened Bun.S3Client endpoint parsing to match new URL().
  • Improved: @types/bun accepts every TextDecoder encoding label the runtime supports, like "windows-1251".

Bugfixes#

Node.js compatibility improvements#

  • Fixed: node:http servers mishandled requests with an empty Transfer-Encoding header, unlike Node.
    • Emitted a spurious clientError after serving the request, so a typical handler destroyed the connection.
    • Rejected the request when Content-Length was also set, instead of reading the body by Content-Length.
  • Fixed: node:http responses emitted 'close' before 'finish' when res.destroy() followed res.end().
  • Fixed: node:http sent non-ASCII header values as UTF-8 instead of latin-1 when the string came from TextDecoder, JSON.parse(), or normalize().
  • Fixed: http.Server.listen(port, cb) never called cb when retried from an EADDRINUSE error handler. This made vite's port auto-increment hang.
  • Fixed: undici.request() rejected a node:stream Readable body. It now streams it.

  • Improved: readline.createInterface() no longer loads node:fs, node:util, or node:vm (58 to 41 internal modules).

  • Improved: server.listen() no longer loads node:cluster in the primary process.
  • Fixed: several node:http2 server and client differences from Node.js.
    • Http2SecureServer lacked closeIdleConnections(), breaking Fastify's forceCloseConnections: "idle" option.
    • session.server was undefined on server sessions, unlike Node.js.
    • Client session.destroy(undefined, code) put code in the GOAWAY frame, so the peer saw an error instead of a clean close.
    • Incoming header values with bytes that are not valid UTF-8 became οΏ½ instead of being read as latin-1 like Node.
  • Fixed: several node:http2 interoperability issues that could make gRPC calls fail against @grpc/grpc-js, grpc-go, and nghttp2 peers, and that garbled non-ASCII header values sent to Node.js. session.altsvc() now passes origin through unchanged, like Node.js.
  • Fixed: net.createServer(cb) did not register cb as a "connection" listener, breaking Mockttp.
  • Fixed: a regression in 1.4.0 where the process exited before a net.Socket connected if unref() or pause() was called first, breaking testcontainers.
  • Fixed: a paused net.Socket or TLSSocket never emitted 'end' when the peer closed the connection, so server.close() hung.
  • Fixed: tls.getCACertificates('system') returned each root certificate 5-6 times on Linux distros that link several CA bundle paths to one file.
  • Fixed: HTTPS requests through a SOCKS5 proxy with undici left one proxy tunnel open per request, unlike Node.
  • Fixed: a TLS 1.3 tls.connect() client whose certificate the server rejected got ECONNRESET instead of a clean close.
  • Fixed: a node:tls write waiting on backpressure failed with ERR_SOCKET_CLOSED when the peer half-closed cleanly, which dropped AWS SDK requests.
  • Fixed: recursive fs.promises.readdir() could hang forever on a tree with directory symlink loops and an entry that fails to open.
  • Fixed: async node:fs calls given a Buffer path could leak the Buffer
  • Fixed: recursive fs.readdirSync() and fs.promises.readdir() threw ENOENT when another process removed a subdirectory during the walk.
  • Fixed: fs.truncateSync(path, undefined) and fs.ftruncateSync(fd, undefined) threw instead of truncating to 0.
  • Fixed: fs.ReadStream and fs.WriteStream threw after Object.freeze(require("node:fs")).
  • Fixed: node:fs callback APIs threw ENAMETOOLONG synchronously for a path over the OS length limit, instead of passing it to the callback.
  • Fixed: a regression in 1.4.0 where fs.rmdir(path, { recursive: true }) threw instead of removing the directory, impacting packages like @prisma/client
  • Fixed: two argument-handling bugs in crypto.sign() and crypto.verify().
    • crypto.sign(null, data, ecKey) threw NO_DEFAULT_DIGEST instead of defaulting to SHA-256.
    • Valid saltLength or padding values were rejected when the number was stored as a double, such as one from JSON.parse().
  • Fixed: reading X509Certificate.publicKey a second time crashed on a certificate whose key could not be decoded.
  • Fixed: setTimeout[util.promisify.custom] was missing until node:util was loaded.
  • Fixed: util.formatWithOptions() accepted a function as options. It now throws, like Node.
  • Fixed: util.parseEnv() and process.loadEnvFile() stored numeric keys like 0=zero so that result[0] was undefined.
  • Fixed: assert failure diffs garbled non-ASCII text, printing οΏ½ for latin-1 characters or lists of numbers for emoji and CJK.
  • Fixed: a regression in 1.4.0 where assert.deepStrictEqual() threw for objects with different prototypes, like Object.create({ x: 1 }) and {}.
  • Fixed: dns.lookup() on macOS failed for some hostnames.
    • A regression in 1.4.0: hostnames served by a VPN, split-DNS, or /etc/resolver returned ENOTFOUND instead of resolving.
    • Names that are not valid hostnames, such as names containing a space, could wait for a 30 second timeout instead of failing with ENOTFOUND.
  • Fixed: dns.resolve(host, "NAPTR") threw ERR_INVALID_ARG_VALUE instead of running a NAPTR query.
  • Fixed: vitest --coverage and c8 could hit "Maximum call stack size exceeded" merging node:inspector coverage from modules with export function declarations.
  • Fixed: the pprof package failed on pprof.time.profile() with an undefined CpuProfiler::StartProfiling symbol.
  • Fixed: when a child process exited before reading all of its stdin, the EPIPE error reported syscall: "send" instead of "write" like Node.
  • Fixed: process.kill(process.pid, "SIGABRT") printed a Bun crash report instead of exiting silently.
  • Fixed: when memory ran out, buffer.toString() and StringDecoder threw ERR_STRING_TOO_LONG instead of Node's ERR_MEMORY_ALLOCATION_FAILED.
  • Fixed: a regression in 1.4.0 where new Worker() from node:worker_threads threw port.on is not a function when a library like happy-dom replaced globalThis.MessagePort.
  • Fixed: an overridden Module._resolveFilename got wrong parent, isMain, and options arguments, breaking proxyquire-style tools.
  • Fixed: after an import of a builtin like https, require.cache exposed its frozen module. This crashed dd-trace with OpenTelemetry.
  • Fixed: several rare N-API addon crashes in finalizers and on process.exit().
    • On the main thread, process.exit() previously ran addon finalizers and cleanup hooks, and these could crash. It now skips them, as Node does.
    • process.exit() with calls still queued on a threadsafe function no longer passes a null env to the addon's callback.
    • A finalizer could still run after napi_delete_reference and crash on freed memory.
  • Fixed: better-sqlite3@13 aborted the process with a NAPI FATAL ERROR on load.
  • Fixed: transferring an N-API external buffer to a Worker could free memory in use.
  • Fixed: on macOS, node:fs calls such as open and readdir could throw EINTR: interrupted system call when a signal arrived mid-syscall, instead of retrying like Node.js.

Bun APIs#

  • Fixed: two server.upgrade() bugs inside a Bun.serve request handler.
    • After an await, it closed Connection: close and HTTP/1.0 sockets and leaked the WebSocket.
    • server.upgrade() and ws.close() ran queued microtasks in the middle of the handler.
  • Fixed: a ServerWebSocket stalled forever when a large send() inside drain hit backpressure.
  • Fixed: Bun.serve() with HTML routes crashed in two cases
  • Fixed: when a Bun.serve handler returned a promise that never settled, an aborted request stayed in server.pendingRequests until garbage collection.
  • Fixed: when an async Bun.serve handler returned a streaming Response after the client disconnected, the stream was not cancelled.
  • Fixed: Bun.serve could crash when the client disconnected while the handler was still running and its promise settled later.
  • Fixed: Bun.serve mishandled responses whose body stream failed.
    • A body stream that errored before its first byte was sent as a complete, empty response.
    • In rare cases, a handler that returned a proxied fetch() response hung instead of running error().
  • Fixed: Bun.serve accepted malformed Range headers with signed or _-separated positions.
  • Fixed: Bun.serve({ app }) accepted wrong-typed options instead of throwing ERR_INVALID_ARG_TYPE.
  • Fixed: Bun.serve and Bun.listen threw a misleading ENOENT for a malformed hostname.
  • Fixed: Bun.file(p).slice(a, b) sent the whole file when its unread .stream() was passed to a Response or bytes(). It also sent the wrong bytes when used as a Bun.serve route.
  • Fixed: blob.writer() and writer.start() silently ignored an invalid path or fd option.
  • Fixed: Bun.sha() silently ignored an invalid output argument instead of throwing.
  • Fixed: Bun.CSRF threw TypeError instead of RangeError for out-of-range expiresIn and maxAge.
  • Fixed: Bun.YAML.stringify with an indent put boxed Number and Boolean values on the line after their key.
  • Fixed: JSON5 errors for bad \x and \u escapes reported the wrong column.
  • Fixed: on the Chrome backend, closing a Bun.WebView created with a url option raised an uncatchable WebView closed rejection.
  • Fixed: Bun.WebView (WebKit backend) hung on navigate() after 64 views in one process.
  • Fixed: Bun.Image on macOS turned undecodable HEIC files into black images and rejected 10-bit HEIC.
  • Fixed: an intermittent JIT bug, mostly seen on Windows, when optimized code passed an ArrayBuffer to a bun:ffi pointer argument.
  • Fixed: toBuffer() and toArrayBuffer() from bun:ffi returned argument errors instead of throwing them.
  • Fixed: on macOS and Linux, Bun.serve buffered a FIFO, socket, or device Bun.file() response body without limit when the client wasn't reading, sometimes crashing if the client then disconnected.
  • Fixed: with --splitting, Bun.serve returned 404 for assets imported only by a module that an HTML import loads via dynamic import(), and for the lazy chunks themselves when there were two or more HTML imports.

Web APIs#

  • Fixed: fetch() with a timeout of 4 seconds or less aborted requests early with TimeoutError, a regression in 1.4.0.
  • Fixed: fetch() with tls.checkServerIdentity opened a new TLS connection for every request, a regression in 1.4.0.
  • Fixed: fetch() gave 204, 205, 304, and HEAD responses a non-null body, so clone() threw after reading.
  • Fixed: new Response(asyncGenerator) appended the generator's return value to the body.
  • Fixed: Headers rejected non-ASCII latin-1 values from JSON.parse or .normalize().
  • Fixed: a streamed fetch() response could lose the end of its body with ECONNRESET when the server closed while the client was still uploading, a regression in 1.4.0.
  • Fixed: new WebSocket("ws://[::1]:PORT") sent a Host header without the port.
  • Fixed: ws handleUpgrade() threw a TypeError when called after an await, a regression in 1.4.0.
  • Fixed: a rare crash when a wss:// WebSocket through a proxy closed mid-write.
  • Fixed: in rare cases, pipeTo() and BYOB read() threw synchronously instead of returning a rejected promise.
  • Fixed: TextEncoderStream crashed on a huge chunk instead of throwing an out-of-memory error.
  • Fixed: new TextDecoder("utf-8", { ignoreBOM: 1 }) threw instead of coercing to a boolean.
  • Fixed: crypto.subtle RSA-PSS accepted saltLength values near 2^32 instead of rejecting them with OperationError.
  • Fixed: JSON.stringify(performance.mark()) omitted detail unless node:perf_hooks was loaded.
  • Fixed: clearTimeout() did nothing when the timer id was an integer stored as a float, such as one read from a Float64Array or returned by Math.sqrt().

Runtime#

  • Fixed: import() of a file mixing import and module.exports threw a confusing SyntaxError.
  • Fixed: require() of a .ts file, or running it directly, failed with "Cannot use import statement" when a type-only import was in the same file as module.exports.
  • Fixed: import() crashed after a user-defined getter on process or Bun loaded an ES module with require() and then threw.
  • Fixed: a Worker leaked 12 KB on exit when its entry point used a package.json imports or exports map.
  • Fixed: reading error.stack leaked memory when running bun build --sourcemap output that has an external .map file.
  • Fixed: memory leaks of strings in module loading, HMR, and several native APIs.
  • Fixed: in rare cases, structuredClone() and postMessage() could return the wrong string when a getter changed the object while it was being cloned.
  • Fixed: a crash when a Worker used an object URL for a named File made in another Worker, or when several Workers got the same BroadcastChannel message with objects.
  • Fixed: a crash when a resizable ArrayBuffer shrank during Bun.zstdCompress() or an fs call.
  • Fixed: in rare cases, a string of 1 to 3 characters could load with the wrong value from the bytecode cache.
  • Fixed: printing an AggregateError without an errors property crashed.
  • Fixed: an error thrown by a mock.module export getter, or by a Proxy trap or toJSON during Bun.inspect, was silently ignored.
  • Fixed: a possible crash when a system call returned an errno Bun did not know, such as one from a FUSE filesystem. Bun reports EUNKNOWN instead.
  • Fixed: an unparseable LANG, LC_ALL, or LC_MESSAGES value crashed Date and Intl calls.
  • Fixed: --inspect crashed when NODE_ENV contained characters like a stray \r.
  • Fixed: piped stdout got ANSI color codes when stderr was a TTY, e.g. bun test | pbcopy.
  • Fixed: process.versions.icu and process.versions.unicode reported the wrong ICU version on macOS.
  • Fixed: the FreeBSD binary failed to load on FreeBSD 15 with a missing libutil.so.9.
  • Fixed: bun --watch crashed on a forced reload when colors were enabled.
  • Fixed: on Linux, a bun --watch reload could in rare cases abort the process if another thread was starting at that moment.
  • Fixed: bun -p printed undefined when bunfig.toml set hoistPattern or publicHoistPattern.
  • Fixed: bun completions and bun upgrade crashed on macOS and Linux when $SHELL was PowerShell.
  • Fixed: the REPL highlighter did not color numeric separators like 1_000 or BigInt n suffixes.
  • Fixed: the REPL froze when typing . after a large Buffer or typed array.
  • Fixed: bun repl left stale rows behind when input wrapped past the terminal width.
  • Fixed: a 1.4.0 regression where join(), toString(), String(arr), or arr | 0 on an array that contains itself threw RangeError: Maximum call stack size exceeded instead of treating the cycle as empty, which broke three.js WebGPURenderer.
  • Fixed: a crash when two signals with process.on("SIG...") listeners were delivered on different threads at the same time, most often on macOS while a Bun.spawn() was in progress.

bun install#

  • Fixed: a no-op bun install with the isolated linker took seconds instead of milliseconds in monorepos where a shared dependency declared a peer that no parent package provided (12.9s β†’ 23ms in one 16-workspace repo).
  • Fixed: bun install hung on "Resolving dependencies" when a transitive peer dependency's manifest was cached from a different registry URL.
  • Fixed: when the connection dropped partway through a tarball or manifest download, bun install printed a misleading error instead of retrying.
  • Fixed: bun install runs that cached the same package manifest at the same moment could lose the cache entry, mostly on Windows.
  • Fixed: bun install silently skipped GitHub dependencies migrated from package-lock.json.
  • Fixed: migrating a pnpm project could corrupt package.json.
    • bun add and bun update -i crashed or wrote invalid content to package.json when pnpm-lock.yaml and pnpm-workspace.yaml were still present.
    • bun install could write corrupted overrides, catalog, and patchedDependencies entries when pnpm-workspace.yaml used quoted or multi-line values.
  • Fixed: bun install panicked when a dependency's package.json declared patchedDependencies.
  • Fixed: bun install failed with "unsafe folder path" when a file: package had a catalog: peer on another file: dependency with an absolute path.
  • Fixed: bun install panicked when a project path was too long to link a package's bins.
  • Fixed: in rare cases, auto-install could crash when a package lookup returned 404 while other modules were loading.
  • Fixed: Ctrl-C during bun install with git dependencies signaled git instead of stopping Bun.
  • Fixed: bun install printed an EISDIR error when .env was a directory, and hung when it was a FIFO.
  • Fixed: bun add of a new git commit for an existing dependency duplicated its package.json key.
  • Fixed: bun add github:user/repo wrote the dependency out of alphabetical order in bun.lock.
  • Fixed: bun install wrote an empty range to bun.lock for an 8-byte range ending in a non-ASCII character, such as 1.0.0-Γ©.
  • Fixed: bun update --global --recursive errored instead of ignoring --recursive as Bun 1.3 did.
  • Fixed: bun prune refused to run after node_modules was deleted and reinstalled with a different linker in a workspace.
  • Fixed: bun pm ls listed a package twice when the root declared it twice, such as a workspace also listed in devDependencies.
  • Fixed: bun pm diff failed with "invalid archive entry size" on tarball files over 64 MiB.
  • Fixed: depending on a workspace via "pkg": "*" when that workspace had no version (or a prerelease version) made every bun install re-resolve it, and bun prune / bun dedupe failed with "bun.lock does not match package.json".
  • Fixed: bun install --backend=copyfile truncated a package's files and its global cache entry to 0 bytes when the node_modules destination was already a hardlink to that cache file from an earlier install.
  • Fixed: bun pm pkg set, bun pm version, bun pack, and bun publish rewrote power-of-ten integers from 10000 to 1000000000 in package.json in exponent form like 1e4.

bun run and bun init#

  • Fixed: bun run --filter, --workspaces, --parallel and --sequential ignored an auto-discovered bunfig.toml unless --config was passed.
  • Fixed: bun run could reuse stale cached output for a file of 4 KiB or more after a bunfig [define] or --drop value changed.
  • Fixed: bun init crashed when an existing package.json had a devDependencies or peerDependencies value that was not an object, like null.
  • Fixed: pressing Ctrl-D at a bun init text prompt printed an internal error.

bun build#

  • Fixed: with several entry points and no --splitting, Bun.build could crash or print the wrong text for a constant-folded string in a shared module.
  • Fixed: in rare cases bun build gave a nested variable the same name as a top-level function, so the output threw "is not a function".
  • Fixed: a 1.4.0 regression where an onResolve plugin returning undefined could make Bun.build() drop code from a sideEffects: false package, causing a ReferenceError.
  • Fixed: with --format=esm, an entry point that re-exported foo from a CommonJS module could clash with another export_foo in the bundle.
  • Fixed: bun build returned all of module.exports for a default import of an __esModule CommonJS module from a .js or .ts file, unlike bun run.
  • Fixed: bun build emitted an empty file for an entry point that only did module.exports = require(...).
  • Fixed: several bun build bugs in the barrel file optimization for sideEffects packages.
    • A module listed in a package's sideEffects array was dropped when it was only reachable through a re-export barrel.
    • Bun.build reported an unresolved re-export twice and ran onResolve plugins more than once for it in sideEffects: false barrels.
    • A sideEffects: false entry point made only of re-exports emitted an export { a } with no declaration.
  • Fixed: a 1.4.0 regression where unused classes with a computed key naming a constant, like [TypeId], were not tree-shaken, bloating Effect bundles.
  • Fixed: with --splitting, bun build could fail with "Multiple files share the same output path" when tree shaking removed all code from two imported files.
  • Fixed: bun build crashed when every entry point was disabled or marked external.
  • Fixed: bun build node:fs and other builtin entry points failed with "File not found" instead of saying builtins cannot be entry points.
  • Fixed: events.once() threw a ReferenceError in bun build --target=browser bundles.
  • Fixed: import "./foo.cjs" did not find foo.cts, and an exact exports or imports target ending in .js did not fall back to the .ts file.
  • Fixed: several bugs with macros in bun build and Bun.build().
    • Macros ignored --define, --loader, and tsconfig settings while bundling.
    • A macro that awaited crypto.subtle.digest() hung the build.
    • A macro returning Response.json() was inlined as a base64 data: URL, not an object.
  • Fixed: reactCompiler added an unused react/compiler-runtime import to components with no memoization.
  • Fixed: Bun crashed on the first JSX element when a tsconfig jsxFactory or jsxFragmentFactory had no identifier in it, like "" or ".".
  • Fixed: with syntax minification, the transpiler mishandled declarations that read members of the same object.
    • var a = o.x, b = o.y became one destructure even when o could change between the reads, like a global with a getter.
    • var n = n.next, n = n.next read both members from the original n.
    • bun build --minify emitted invalid code for using a = obj.x, b = obj.y inside a function.
  • Fixed: Bun.Transpiler#scanImports garbled non-ASCII import specifiers.
  • Fixed: --metafile, bun audit --json, and bun pm view --json wrote invalid JSON when a string held malformed UTF-8 or a lone surrogate.
  • Fixed: bun build on Windows wrote backslash-separated asset paths and HTML manifest entries.
  • Fixed: dev server <img> URLs in HTML imports returned 404 after editing the HTML file.
  • Fixed: dev server crashed when editing a file imported from outside the project root.
  • Fixed: bun index.html crashed on an anonymous export default class extends React.Component {}.
  • Fixed: bun build without --splitting wrote only one output file when several entry points imported each other. When one entry point imported another, modules in its output could also run in the wrong order.
  • Fixed: with --splitting, await import() of a CommonJS module in another chunk (e.g. react-dom/client) resolved to { default } only, so named exports like createRoot were undefined.
  • Fixed: bun build produced broken output for import * as ns of a CommonJS module that assigns exports.x = ...:
    • ns.method() calls, and exports.fn() calls inside the module itself, dropped this, so a method that read this._helper threw undefined is not an object (e.g. stack-trace).
    • Modules that also read module.constructor, module.hot, or module.paths threw ReferenceError: module_lib is not defined (e.g. pirates).
    • Modules that declared a var with the same name as one of their top-level functions threw SyntaxError: Cannot declare a var variable that shadows a let/const/class variable when the ESM bundle loaded.
  • Fixed: bun build resolved ns[Enum.member] on an import * as ns namespace to the wrong export when the const enum member's value was a concatenated string like "a" + "b" (it read export a instead of ab).
  • Fixed: bun build tree-shaking removed an unused destructuring declaration like const { x } = obj even when the destructuring could run a getter or iterator with side effects.
  • Fixed: Bun.build ignored the path an onResolve plugin returned with external: true, so a plugin could not rewrite an external import to a different path or URL.

bun build --compile#

  • Fixed: bun build --compile binaries for darwin-arm64 in rare cases failed codesign -v and were killed on macOS 27.
  • Fixed: UPX-compressed bun build --compile executables failed on Linux with Invalid character: '\0'.
  • Fixed: a 1.4.0 regression where bun build --compile failed with EACCES when run from a WSL2 /mnt/c path.
  • Fixed: bun build --compile for macOS panicked instead of erroring when the executable exceeded 4 GiB.
  • Fixed: a 1.4.0 regression where Bun.build({ compile: true }) with no outfile or outdir wrote the executable next to the entrypoint, not the working directory.
  • Fixed: new Worker(new URL("./worker.ts", import.meta.url)) could not find the worker in bun build --compile executables.
  • Fixed: Bun.Glob scan() threw ENOENT on embedded directories in compiled executables.
  • Fixed: bun build --compile executables ran with the JIT and GC throttled when their own arguments included -e, -p, --eval or --print.
  • Fixed: bun build --bytecode intermittently aborted on large builds.
  • Fixed: --compile --bytecode chunks with non-ASCII text silently skipped their bytecode cache.
  • Fixed: in a --compile --splitting executable, import() of the entry point from another chunk failed with Cannot find module '/$bunfs/root/<entry>.js'. The entry point's external source map is now written to <outfile>.map.
  • Fixed: bun build --compile targeting Linux with more than 4 GiB of embedded files produced an executable that failed at startup with Module not found ''. The build now errors instead.

JavaScript minifier#

  • Fixed: in rare cases Bun.build({ minify: true }) produced different identifier names across identical builds of a project with sideEffects: false barrel files.

CSS Parser#

  • Fixed: bun build warned on valid ::details-content, ::picker(), ::checkmark and ::picker-icon selectors.
  • Fixed: CSS modules did not scope view-transition-name and ::view-transition-group() names.

bun test#

  • Fixed: in rare cases, bun test --parallel hung forever when a worker process exited before it finished starting up.
  • Fixed: bun test --parallel --coverage under-reported function coverage for a file when different workers ran different functions in it.
  • Fixed: on Windows, bun test --parallel or --isolate could crash when more than one test file used Bun.WebView.
  • Fixed: Bun.spawn and child_process with piped stdio failed with EBADF inside bun test on macOS in repos with thousands of directories.
  • Fixed: under jest.useFakeTimers({ now }) and setSystemTime(), performance.timeOrigin stayed on the real clock, so timeOrigin + performance.now() did not match Date.now().
  • Fixed: mock.module() silently swallowed errors thrown by a Bun.plugin onResolve hook.
  • Fixed: mockResolvedValue() and mockResolvedValueOnce() swallowed the error when reading the value's constructor property threw.
  • Fixed: this.utils.printReceived() in an expect.extend matcher crashed when a custom inspect threw.
  • Fixed: when a Set, Map, WeakSet, or WeakMap had a non-numeric own size property, a failing toEqual() printed a bare TypeError instead of the diff.
  • Fixed: expect(x).toBeWithin(a) with one argument crashed bun test instead of failing.
  • Fixed: when the toSatisfy() predicate threw, bun test wrapped the error in an AggregateError that could crash console.log. It now rethrows the predicate's error.
  • Fixed: toMatchObject() and Bun.deepMatch() crashed on objects nested thousands of levels deep instead of throwing RangeError.
  • Fixed: JUnit reports and GitHub Actions annotations crashed when an error's stack included a long data: URL module or a long //# sourceURL= name.

Bun Shell#

  • Fixed: Bun Shell split export NAME=$(cmd) output on whitespace instead of keeping one value.
  • Fixed: a Bun Shell pipeline hung forever when it could not create its pipes because the process was out of file descriptors (EMFILE).
  • Fixed: Bun Shell ls errored on a dangling symlink operand, and ls file/x printed the path instead of a "Not a directory" error.
  • Fixed: after await $`cmd > ${buf}` , the ArrayBuffer could stay pinned if the child's stdout closed after it exited, for example when a grandchild held it open.

SQL / SQLite / S3 clients#

  • Fixed: Bun.sql decoded Postgres float8 and float4 Infinity values as NaN in text-format results, such as .simple() and sql.unsafe() without parameters.
  • Fixed: Bun.sql returned Invalid Date in .simple() queries and arrays for Postgres timestamptz values with a seconds offset, like -04:56:02 for pre-1900 dates.
  • Fixed: Bun.sql could crash when close(), ref(), or unref() ran after the server had already dropped the connection.
  • Fixed: sql.close() fired onclose for pool connections that never connected.
  • Fixed: Bun.sql stored Date values in MySQL TIMESTAMP columns at the wrong instant when the session time zone was not UTC.
  • Fixed: in bun:sqlite, db.prepare() with params crashed on SQL that held no statement (only whitespace, comments, or ;) instead of throwing Invalid SQL statement.
  • Fixed: a streaming S3 upload from a ReadableStream could fail if garbage collection ran while the upload waited for a part to finish.
    • The upload could crash or hang.
    • A later error from the source stream was lost.

TypeScript types#

  • Fixed: @types/bun broke Event.composedPath() types when lib: ["dom"] was enabled.
  • Fixed: with @types/node@24 installed, process.off() and process.removeListener() rejected every event name except memoryPressure.
  • Fixed: TypeScript rejected fetch(url, { protocol: "h3" }). Bun supports it at runtime.
  • Fixed: TypeScript parse errors on a ? (b) : c => d, export type followed by a newline, and import type from.

Windows#

  • Fixed: on Windows, bun install workspace pattern errors and some other error messages said NOENT instead of ENOENT.
  • Fixed: on Windows, Readable.fromWeb(Bun.file(path).stream()) could crash while reading a file larger than the stream buffer.
  • Fixed: on Windows, fs.copyFile reported success when the copy failed with an error code Bun did not recognize, such as a missing network share.
  • Fixed: on Windows, process.chdir() during a recursive fs.rm could close an unrelated handle.
  • Fixed: on Windows, child_process.spawn() with extra stdio pipes could close unrelated handles.
  • Fixed: on Windows, Bun leaked memory when connecting to a named pipe failed.
  • Fixed: on Windows, a named-pipe socket could crash if its data or end handler closed it and then ran the event loop, as expect().resolves does.
  • Fixed: on Windows, closing process.stdin or Bun.stdin.stream() while a console read was pending could corrupt memory and cause a later crash.
  • Fixed: on Windows, child_process, Bun.spawn, and Bun.which could not find .com executables like chcp. child_process regressed in 1.4.0.
  • Fixed: bun run exited as soon as Ctrl+C was pressed, before the script finished handling it. This affected Windows and --shell=bun.
  • Fixed: on Windows arm64, in rare cases a process that used WebAssembly could hang on exit.
  • Fixed: on Windows Server 2019, the crash report said "CPU lacks AVX support" and omitted the CPU: line on CPUs that do support AVX.

Thanks to 7 contributors!#