Blog

Bun v1.4.3

To install Bun

curl

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

To upgrade Bun

bun upgrade

bun check: a TypeScript type checker built into Bun#

bun check, a TypeScript type checker built into Bun, is 3x to 7x faster than tsc 7. Type checking VS Code, 10,947 files: bun check takes 1.75 s and 2.77 GiB, tsc 7 with --checkers 16 takes 5.88 s and 9.23 GiB, and tsc 7 (typescript-go) with default settings takes 12.58 s and 7.73 GiB. That is 2.8x to 3.3x less memory. Linux x64, 64 threads, mean of 20 runs. bun check passes 100% of TypeScript's conformance tests and is a port of typescript-go.

bun check is an incredibly fast TypeScript type checker that passes 100% of TypeScript 7.0.2's conformance test suite.

bun check
1 | import { greet } from "./user";
2 |
3 | const message = greet({ id: "1", name: "Ada" });
                            ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at src/index.ts:3:25

Found 1 error in 1 file, checked 2 files [14.00ms]

It's 3x to 6.4x faster than tsc 7.0.2, on a 16-core Apple silicon Mac:

ProjectFilesbun checktsc 7.0.2Faster
VS Code src9,7951.24 s5.98 s4.8x
mikro-orm2,8831.20 s5.97 s5.0x
Next.js packages/next2,8810.28 s1.82 s6.4x
Next.js, root3,5470.42 s1.28 s3.1x
Storybook scripts1,0390.27 s0.96 s3.5x
Nuxt8390.27 s0.80 s3.0x
Playwright7060.15 s0.64 s4.2x
lit packages/react60.46 s2.12 s4.6x

And it uses 2.2x to 4.9x less memory:

ProjectFilesbun checktsc 7.0.2Less
VS Code src9,7952.14 GB7.97 GB3.7x
mikro-orm2,8831.35 GB6.67 GB4.9x
Next.js packages/next2,8810.71 GB1.81 GB2.5x
Next.js, root3,5470.55 GB1.33 GB2.4x
Storybook scripts1,0390.56 GB1.39 GB2.5x
Nuxt8390.51 GB1.14 GB2.2x
Playwright7060.46 GB1.06 GB2.3x
lit packages/react60.24 GB0.63 GB2.6x

It's a port of typescript-go. It reads your tsconfig.json, reports the same errors as tsc, and uses every CPU core. You don't need the typescript package installed.

It only type checks. It doesn't emit JavaScript or .d.ts files, and there's no language server, so your editor keeps using TypeScript.

bun run --check#

Type check, then run. If there's a type error, your code doesn't run.

bun run --check src/index.ts
1 | import { greet } from "./user";
2 |
3 | console.log(greet({ id: "1", name: "Ada" }));
                        ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at src/index.ts:3:21

2 |   id: number;
      ^
note: The expected type comes from property 'id' which is declared here on type 'User'
   at src/user.ts:2:3

Found 1 error in 1 file, checked 2 files [24.00ms]

It also works with package.json scripts (bun run --check dev) and with --watch, which checks again before every restart.

bun test --check#

Type check the test files and everything they import, then run the tests. If there's a type error, no tests run.

bun test --check
bun test v1.4.3
4 | test("greet", () => {
5 |   expect(greet({ id: "1", name: "Ada" })).toBe("hello Ada");
                     ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at src/user.test.ts:5:18

2 |   id: number;
      ^
note: The expected type comes from property 'id' which is declared here on type 'User'
   at src/user.ts:2:3

Found 1 error in 1 file, checked 2 files [23.94ms]

bun build --check#

Type check, then bundle. A type error fails the build like any other build error, and nothing is written.

bun build --check ./src/index.ts --outdir out
3 | console.log(greet({ id: "1", name: "Ada" }));
                        ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at /app/src/index.ts:3:21

2 |   id: number;
      ^
note: TS6500: The expected type comes from property 'id' which is declared here on type 'User'
   at /app/src/user.ts:2:3

Bun.build takes check: true.

Testing bun check#

Every commit of Bun runs TypeScript 7.0.2's complete conformance test suite (and we will continue to keep it up-to-date as TypeScript itself updates).

BaselineWhat it checksTestsPassing
ErrorsEvery error13,10113,101
TypesThe type of every expression12,46312,463
SymbolsThe symbol behind every name12,46312,463
DeclarationsEmitted .d.ts files13,03213,032
Module resolutionHow every import resolves151151
Total51,21051,210

72 misconfigured open-source projects

For a project as complex as a TypeScript type checker, conformance tests aren't enough, so we deliberately misconfigured 72 popular open-source repositories and compared bun check's output with tsc's output.

Missing node_modules, mismatched TypeScript versions, no build step. Every error has to match: same file, line, column and error code.

ProjectConfigsIdenticalErrorsMismatches
anthropics/anthropic-sdk-typescript16165960
apollographql/apollo-client775590
arktypeio/arktype553890
axios/axios111050
colinhacks/zod10101910
date-fns/date-fns3310
discordjs/discord.js2230
drizzle-team/drizzle-orm3310,7860
Effect-TS/effect101020
elysiajs/elysia44530
excalidraw/excalidraw7711,0480
fabian-hiller/valibot2210
fastify/fastify1100
gcanti/fp-ts2230
gcanti/io-ts2230
graphql/graphql-js33240
gvergnaud/ts-pattern2220
honojs/hono6614,2390
immerjs/immer1150
jquense/yup1110
kysely-org/kysely661,1850
langchain-ai/langchainjs41415,2730
lit/lit34342,5880
microsoft/playwright44220
microsoft/TypeScript446130
microsoft/vscode, its own build1100
microsoft/vscode, extensions and tests1051055,6850
mikro-orm/mikro-orm171787,9810
millsp/ts-toolbelt1130
mobxjs/mobx331860
mswjs/msw10105,8780
nestjs/nest303027,3450
nuxt/nuxt33110
openai/openai-node4460
pmndrs/jotai2270
pmndrs/valtio1100
pmndrs/zustand1100
preactjs/preact1110
prisma/prisma444424,2010
puppeteer/puppeteer10102,7010
react-hook-form/react-hook-form2230
ReactiveX/rxjs101022,2710
reduxjs/redux1100
reduxjs/redux-toolkit881420
remeda/remeda77130
remix-run/react-router14146,7350
rollup/rollup991070
sequelize/sequelize16166,7680
sindresorhus/got1120
sindresorhus/ky22520
sindresorhus/type-fest2200
solidjs/solid777400
statelyai/xstate885450
storybookjs/storybook19191,4010
stripe/stripe-node44420
sveltejs/svelte551860
TanStack/form13131240
TanStack/query15156860
TanStack/router29294,8530
TanStack/table20205740
tldraw/tldraw4400
total-typescript/ts-reset2240
typeorm/typeorm449590
typescript-eslint/typescript-eslint353518,3920
unjs/h31100
unjs/nitro557420
urql-graphql/urql22170
vercel/ai52529,7420
vercel/next.js3563563,6900
vercel/swr443140
vitest-dev/vitest18182,1110
vuejs/core651,3591
withastro/astro12121,9960
All 721,1031,102286,2671

The one mismatch is in vuejs/core. tsc only reports that error when global.ts comes before model.ts in files.

We ran 69 of them again under 11 other sets of compiler options:

Compiler optionsProjectsIdenticalErrorsMismatches
Every strictness flag on6969100,7560
strict off696876,96012
skipLibCheck off696990,6090
checkJs696984,1220
declaration696980,4950
isolatedModules + verbatimModuleSyntax696980,6220
isolatedDeclarations626234,9820
erasableSyntaxOnly + legacy decorators696971,5770
nodenext696985,2380
"types": []6969105,8180
"lib": ["es5"]696983,8720
All 11752751895,05112

Fuzzing & mutation testing

Each fuzzer generates every combination of a set of language features, runs tsc and bun check on the result, and diffs the output.

FuzzerFunctionsMismatches
Control flow narrowing46,0800
Variables with no type annotation29,4840
Variables reassigned inside 12 kinds of loop20,3040
Callbacks and contextual typing9,9840
Circular references9,1960
Interfaces that extend a mapped type of themselves8,4240
Index signatures5,7700
Circular references next to overloaded calls3,8880
Boolean assignability1,6640
8 smaller fuzzers1,4410
Total136,2350

Those run on every commit. These are larger runs we did offline:

FuzzerProgramsMismatches
Overloads, inference and variance1,300,0000
Where long types get cut off in error messages1,060,0000
Classes: access, overrides, initialization, super290,0000
Duplicate declarations of a variable or property123,8910
JSDoc in type-checked JavaScript68,3340
JSX: tags, attributes and children63,7700
Annotations that refer to what's being declared50,68895
Class expressions that refer to what holds them40,12821
Immediately invoked functions19,20021
Symbols that tsc creates lazily15,44425
Infinitely recursive type aliases51728
Module augmentation through re-exports2101
Total3,032,182191

162 of the 191 mismatches are in code that refers to itself while it's being declared, like class A { x = { a: null! as { p: A["x"] } } }.

For mutation testing, we broke Effect's source 913 times (swapped arguments, dropped type arguments, deleted overloads) and compared every error.

MutationsErrors in tscMissing in bun checkOnly in bun check
9132,92500
Also on every commitCount
Fuzzer-generated functions, diffed against tsc136,235
Edge cases found by reading typescript-go next to the port, diffed against tsc2,362
Corrupted source files that still have to report an error1,893
Generated projects: file name casing, malformed tsconfig.json, references656
Regression tests576

Try it on your project

bunx -p typescript@7.0.2 tsc --noEmit --pretty false > tsc.txt
bun check --no-pretty > bun.txt
diff tsc.txt bun.txt

If there's a difference, that's a bug in Bun. Please open an issue.

Read the bun check docs.

New in the runtime#

67–89% less CPU while your server sleeps#

Bun v1.4.3 uses 67–89% less CPU than v1.4.2 while your server sleeps. In one idle minute, v1.4.2 woke up every second, because any occasional request kept the 1-second GC timer running. v1.4.3 wakes only for the health check, every 10 seconds. CPU ms in 2 idle minutes on v1.3.14, v1.4.0, v1.4.2 and v1.4.3: Express 430, 308, 282, 57. Fastify 450, 284, 267, 57. Elysia 473, 280, 197, 21. Hono 417, 263, 195, 21. Next.js SSR 7,029, 628, 815, 268. 6–11% less idle memory than v1.4.2. Measured after a 30 s load test with one health check every 10 s, on Linux x64.

Bun.FetchSession#

Bun.FetchSession gives a group of requests their own TLS, proxy, and keep-alive settings and their own connection pool. Sessions never share connections with each other or with plain fetch().

const session = new Bun.FetchSession({
  tls: { ca: await Bun.file("corp-ca.pem").text() },
  proxy: { url: "http://proxy.internal:8080" },
  keepAlive: { idleTimeout: 30, maxIdleSockets: 8 },
});

const client = new SomeClient({ fetch: session.fetch });
await fetch("https://example.com", { session }); // same thing
  • session.fetch is bound, so any client that accepts a fetch function can use it.
  • Options on the request override the session's.
  • session.close() (or using) closes idle connections.
  • A session's tls.checkServerIdentity runs once per connection. Requests that reuse the connection skip it.

Better proxy environment support in fetch()#

NO_PROXY accepts wildcards, CIDR blocks, bare IPv6 addresses, and host:port.

NO_PROXY="*.internal.example.com, 10.0.0.0/8, fd00::/8, localhost:3000"

ALL_PROXY is used when HTTP_PROXY / HTTPS_PROXY is unset. proxy: false ignores the proxy environment for one request.

await fetch("https://example.com", { proxy: false });

When a proxy refuses CONNECT with a non-2xx status, fetch() rejects with ERR_PROXY_TUNNEL. The error carries the proxy's status and headers. Before, fetch() resolved with the proxy's reply as if it came from the origin.

Experimental: Bun.ModuleGraph#

Run many instances of one app in a single process. Each graph gets fresh module state, its own require.cache, and its own timers and I/O. Parsed code and bytecode are shared between graphs.

const graph = new Bun.ModuleGraph({
  globals: { process: tenantProcess },
  onError: (error, kind) => console.error(kind, error),
});

const app = await graph.import("./app.ts");
await graph.run(() => app.handle(request));

graph.dispose(); // closes the graph's servers, sockets, timers, child processes
  • graph.run(fn, ...args) calls fn in the graph's context, so what it opens belongs to the graph.
  • graph.dispose() (or using) behaves like worker.terminate(). It is not a security sandbox.
  • Bun.ModuleGraph.current is the graph whose context the caller is in.

--disallow-code-generation-from-strings blocks eval() and new Function()#

With Node's --disallow-code-generation-from-strings flag, eval() and new Function() throw. It covers the whole process, including every Worker. Bun used to accept the flag and ignore it.

new Function("return 1 + 1");
// EvalError: Code generation from strings disallowed for this context

=strict is Bun-only. It also blocks every other way a string becomes code: node:vm, import() of a data: URL, new Worker(code, { eval: true }), module._compile(), plugins that return source text, napi_run_script() and the inspector.

A flag embedded in a compiled executable always applies. BUN_OPTIONS can raise the level but not lower it.

bun build --compile ./server.ts --outfile server \
    --compile-exec-argv="--disallow-code-generation-from-strings=strict"

Up to 2.7x faster large responses in node:http#

A response over 16 KB now goes out in one write per tick instead of up to 16. It applies to node:http and Bun.serve, over HTTP and HTTPS.

Requests per second from a node:http server:

ResponseAfterBeforeNode.js 26.3
4 Γ— res.write(16 KB)19,1606,98317,735
4 Γ— res.write(16 KB), HTTPS15,5106,11011,720
40 Γ— res.write(2 KB)14,9806,1409,879
res.end(64 KB)19,88916,52818,535

node:http was slower than Node in six of eight large-response benchmarks. Now it's faster in all eight. A Bun.serve direct stream writing 4 Γ— 16 KB is 83% faster. Small responses are unchanged.

JavaScriptCore upgrade#

Bun's JavaScriptCore engine picks up upstream WebKit performance work and spec fixes.

OperationChange
JSON.parse() on npm registry manifests1.2x–1.5x faster
JSON.parse() on 1,000 objects with the same keys1.5x faster
/^\/users\/(\d+)$/.test(path) in a router benchmark~5x faster
Object.values(obj) with 9+ properties2.3x–3.2x faster
arr.copyWithin(0, 1)~2x faster, ~45x with holes
arr.includes(x) on long number arrays2.4x–3.3x faster
arr.splice(2, 1, x)new fast path
for (const x of arr), and the same for stringsno iterator object allocated
const [a, b] = arrno iterator object allocated

The JSON.parse numbers are against Bun v1.4.2 on an Apple silicon Mac, with manifests from 331 KB to 8 MB. The for...of and destructuring change helps code that hasn't reached the top JIT tier yet.

  • WebAssembly wide arithmetic (i64.add128, i64.mul_wide_u and friends) is on by default.
  • RegExp.escape() no longer escapes supplementary code points like U+2002A.
  • Atomics.isLockFree(4294967297) returns false instead of wrapping to 1.
  • A strict-mode async generator that does return someCall() awaits the returned promise.
  • ++arr.length in a loop no longer slows down quadratically past 100,000 elements.
  • BigInt("-"), BigInt("+") and BigInt("0x") throw SyntaxError instead of returning 0n.
  • (a?.b)(x) throws a TypeError when a is null or undefined. Before, it returned undefined, as if the parentheses weren't there.
  • super.tag`x` calls tag with the current this.
  • Error.stackTraceLimit is read from the Error constructor when a stack is captured, like V8. node:vm contexts start at 10, matching Node.
  • JIT-optimized code with many live floating-point values no longer turns one into NaN after reading arr[i]. This could happen if arr was sometimes a Float32Array or Float64Array and sometimes another kind of object.
  • Intl.DurationFormat keeps the minus sign of a duration between -1 and 0. With seconds: "numeric", { milliseconds: -500 } formats as -0.5 instead of 0.5.
  • Intl.Segmenter's containing(index) returns the right segment when index is the first half of a surrogate pair, like the start of an emoji.
  • On Windows, WebAssembly fast memory is enabled again. Growing a WebAssembly memory or a resizable ArrayBuffer at the system commit limit throws instead of crashing.
  • --bytecode output is 3–6% smaller.

Faster handling of many rejected promises#

Promise.allSettled over thousands of throwing async calls no longer stalls. Attaching handlers to many promises that reject in the same turn took quadratic time. It is linear now.

await Promise.allSettled(
  Array.from({ length: 100_000 }, async () => {
    throw new Error("nope");
  }),
);
Time
After148 ms
Before24 s
Node.js475 ms

Attaching .catch() to 160,000 already-rejected promises went from 83 s to 24 ms.

CompressionStream accepts a compression level#

Pass a level option to CompressionStream to trade ratio for speed. Brotli previously always ran at quality 11, which is very slow for large, dynamic responses.

const stream = new CompressionStream("brotli", { level: 4 });
  • gzip, deflate, deflate-raw: 0-9
  • brotli: 0-11
  • zstd: 1-22

Out-of-range values throw a RangeError. Omitting level keeps each format's current default.

Bun.serve supports If-Range#

Clients can safely resume a download from Bun.serve. A Range request with If-Range gets the range only if the file hasn't changed. Otherwise it gets the full body.

await fetch("http://localhost:3000/big.bin", {
  headers: {
    Range: "bytes=1000-",
    "If-Range": lastModified, // from the first response
  },
});
// 206 if the file is unchanged, 200 with the full body if it changed

It works for Bun.file responses and { dir } routes.

New in bun install#

bun install skips tarballs it won't install#

Without a lockfile, bun install and bun add no longer download tarballs for packages that never land in node_modules: bundled dependencies, packages for other platforms, and groups turned off by --production or --omit.

Tarballs downloaded with a cold cache and no lockfile:

InstallAfterBefore
npm@10.9.21196
express + dev deps, --production69403

New in bun build#

Lower memory usage in bun build with many entry points#

Builds with many entry points or chunks use much less memory.

Peak RSS, 800 entry pointsAfterBefore
Each imports one export from a file with 200,000 exports, --splitting276 MB2,121 MB
13 modules each, with or without --splitting200 MB405 MB

This fixes a regression from Bun v1.4.1 where bun build --splitting with ~800 chunks peaked at 7.7 GB instead of 6.2 GB. On Windows, it could abort with memory allocation of N bytes failed.

Bun.build supports [hashN] in output names#

Large --splitting builds no longer fail with "Multiple files share the same output path". Before, two chunks with different contents could get the same 8-character [hash] name. Now both names get extra hash characters until they differ.

Use [hash9] through [hash13] to set a wider minimum hash. [hash13] is the full 64-bit hash.

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  splitting: true,
  naming: { chunk: "chunk-[hash13].[ext]" },
});

Metafile names the input file behind a split import()#

With --splitting, each dynamic import() in metafile.inputs has an entryPoint field naming the input file it loads, so tools that build a module graph no longer have to join through outputs. Metafile byte counts and --metafile-md output are now accurate too.

meta.json

{
  "path": "./chunk-abc123.js",
  "kind": "dynamic-import",
  "original": "./lazy.js",
  "entryPoint": "lazy.js",
  "external": true
}
  • entryPoint is a key of inputs. A require() of an ES module with --target=bun gets one too. True externals like import("node:fs") don't.

New in bun build --compile#

bun build --compile supports profile-guided bytecode layout#

--bytecode-order lays out a --compile --bytecode executable from a profile of a real run. The bytecode that run used goes together at the front of the file.

In one large CLI app, cold start went from 1.01s to 0.53s. Resident bytecode memory at its prompt went from 59.5 MB to 20.5 MB.

bun build --compile --bytecode ./cli.ts --outfile myapp

# Run it the way your users do. The profile is written on exit.
BUN_BYTECODE_ORDER_OUT=myapp.order ./myapp --help

bun build --compile --bytecode --bytecode-order=myapp.order ./cli.ts --outfile myapp
  • Functions are matched by a hash of their syntax, so a profile from an older build still applies.
  • --bytecode-order=a.order,b.order takes several profiles, most common first.
  • Bun.build uses compile: { bytecodeOrder: "./myapp.order" }.
  • bytecodeOrderStats() from bun:jsc reports how many loaded functions the profile covered.

Faster startup for --compile --bytecode executables#

Executables built with bun build --compile --bytecode start faster. ESM executables embed a pre-resolved module graph and optimized bytecode and load every bundled module in one pass, and JavaScriptCore does less work to decode embedded bytecode and link compiled functions.

On a ~2,300-module CLI app, time to the first interactive prompt went from 698 ms to 579 ms (βˆ’17%). On a ~2,000-module CLI, the bytecode decode changes alone cut 6.3% of instructions to its first prompt.

bun build ./app.ts --compile --bytecode --format=esm --outfile myapp

The new --compile-jit-policy <n> flag (compile: { jitPolicy: n } in Bun.build) scales JIT tier-up thresholds. Run-once startup code stays in the interpreter longer. Call Bun.unsafe.setJITPolicy(1) once your app is interactive:

await renderFirstScreen();
Bun.unsafe.setJITPolicy(1); // back to the normal JIT policy
  • --no-optimize-bytecode / optimize: { bytecode: false } skips the build-time bytecode optimizer.

Other improvements#

  • Fixed: crypto.randomInt() was about 20x slower per call since Bun 1.4.0. It takes 34 ns instead of 760 ns, on par with Node.js.
  • Fixed: small zstd calls were up to 3.7x slower in x64 virtual machines since Bun v1.4.0. Bun.zstdDecompressSync on a 1 KB input takes 1.38 Β΅s instead of 5.11 Β΅s.
  • Fixed: node:zlib calls on small inputs were slower since Bun v1.4.0. gunzipSync with a 1 KB result takes 2.97 Β΅s instead of 4.99 Β΅s, and 64 MB through gzip streams takes 131 ms instead of 166 ms.
  • Fixed: Bun.color() got slower in Bun v1.4.0, and this recovers most of that. A call takes 208 ns instead of 692 ns, and 432 ns instead of 17,793 ns when 8 Workers call it at once.
  • Improved: a short transpiler.transformSync() call takes 550 ns instead of 1,236 ns, and 2,290 ns instead of 15,246 ns when 8 Workers call it at once.
  • Improved: vm.runInContext() and vm.runInNewContext() are about 30% faster per call (2.5 Β΅s to 1.7 Β΅s). new vm.Script(src, {}) is about 2x faster. This recovers most of a 1.4.0 regression.
  • Improved: Bun.serve answers pipelined HTTP/1.1 requests with one send() for the whole batch instead of about 14 syscalls per response. This fixes a regression from Bun v1.2.6.
  • Improved: Object.keys(require.cache) and key in require.cache no longer leak a namespace object per loaded ES module. Listing 2,000 modules dropped from 38 ms to 2.6 ms.
  • Improved: Bun.markdown.render() no longer takes quadratic time on deeply nested lists or emphasis when no callback is registered for the nested element. In Bun v1.4.2, 300 KB of nested lists took 99.5 s.
  • Improved: Bun.gc(true) and gc() return freed memory to the OS before returning, so RSS drops right away instead of after a delay. This holds even when a background purge is in progress, and memory freed during a purge no longer stays resident until the event loop goes idle.
  • Improved: Bun's mimalloc fork is now slightly smarter about when to give freed memory back to the operating system. Echoing a 512 KiB JSON body, Elysia takes 388 page faults per request instead of 574, and Express 523 instead of 762.
  • Improved: threads that are idle after WebAssembly.compile(), or blocked in Atomics.wait() or Bun.sleepSync(), give freed memory back to the OS after about 100 ms. A thread blocked for one second after freeing most of its heap held 115 MB instead of 424 MB.
  • Improved: on macOS, process.memoryUsage().rss now reports the physical memory footprint that Activity Monitor shows, and process.resourceUsage().maxRSS reports its peak.
  • Improved: long-running bun build --compile apps on Linux keep less of Bun's own code resident in memory. Bun's linker order file now also traces app-shaped workloads.
  • Improved: on Windows, Bun.secrets.set() takes persist: "local" to keep a credential on the current computer. By default ("enterprise") it roams with the user's account.
  • Improved: updated SQLite to 3.53.4, libarchive to 3.8.9, brotli to 1.2.0, lsquic to 4.9.4 and libjpeg-turbo to 3.2.0. Chunked HTTP bodies with bare-LF chunk line endings are now rejected, as in llhttp.
  • Improved: updated the bundled root certificates to NSS 3.128 (Firefox 156). This adds SECOM and Telia TLS roots and removes Entrust Root Certification Authority, ePKI, Atos TrustedRoot 2011, and SecureSign Root CA12.
  • Improved: the oven/bun:alpine Docker image is now based on Alpine 3.24, matching the libstdc++ the musl build links against.
  • Improved: bun test --coverage-reporter=lcov now implies --coverage. Before, it ran the tests and wrote no report.

Bugfixes#

Security#

  • Hardened: fetch, WebSocket, Bun.connect, Bun.RedisClient and Bun.SQL verify the server during the TLS handshake instead of after it, like curl and Go. Error codes are unchanged.

  • Hardened: TLS certificate name matching in tls.connect, tls.checkServerIdentity, https, fetch and Bun.connect only accepts valid host names and canonical IP addresses (CVE-2026-48618).

  • Hardened: certificate verification in node:tls and Bun.listen when a connection closes during the TLS handshake.

  • Hardened: certificate checks in node:tls, https.Agent and fetch() when a TLS session or pooled connection is reused. fetch() rejects a tls.checkServerIdentity that isn't a function or that returns a truthy value, as in Node.

  • Hardened: Bun.SQL treats tls: Bun.file("ca.pem") (or ssl:) like tls: { ca: Bun.file("ca.pem") } and uses verify-full.

  • Hardened: node:http2 servers rate-limit stream resets (CVE-2023-44487, CVE-2025-8671). streamResetBurst and streamResetRate take effect, matching Node.

  • Hardened: Bun.serve follows RFC 9112 more strictly after Connection: close.

  • Hardened: rare scenario when precise conditions are met that can cause req.url and req.headers to return incorrect values on aborted requests in Bun.serve().

  • Hardened: TLS Bun.serve() servers apply the idle timeout to connections that never finish the handshake.

  • Hardened: fetch() validates chunked transfer encoding in responses more strictly, like Node.

  • Hardened: fetch("file://host/path") rejects with ERR_INVALID_FILE_URL_HOST unless the host is empty or localhost.

  • Hardened: fetch() rejects when your code passes a Content-Length header that doesn't match the ReadableStream body it sends.

  • Hardened: fetch() and node:http agents terminate an idle keep-alive connection that received unexpected data, matching Node.js.

  • Hardened: fetch() against servers that close or reset a TLS connection at unexpected times. This fixes three rare crashes.

  • Hardened: bun install parses registry and tarball URLs more strictly before attaching credentials.

  • Hardened: bun install against malformed bin fields in a package.json or registry manifest.

  • Hardened: Bun.Archive#extract(), bun create and bun install of github: dependencies against malformed tar archives.

  • Hardened: v8.deserialize() and IPC with serialization: "advanced" against crafted records.

  • Hardened: UDP socket addMembership(), dropMembership() and their source-specific variants against arguments with side effects.

Node.js compatibility improvements#

node:http

  • Fixed: after req.pause() in a node:http server, a small body was not received until req.resume(), so req.complete stayed false and req.readableLength stayed 0 while paused.
  • Fixed: in a node:http server, res.end() before the request body had arrived made req emit 'end' at once and dropped the rest of the body. A handler that read the body first was unaffected.
  • Fixed: in a node:http server, a synchronous res.destroy() in the 'request' listener or a 'data' listener, while the request body was still arriving, made req emit 'end' with req.complete === true instead of 'aborted' and ECONNRESET.
  • Fixed: 'finish' and the res.end() callback on a node:http response fired before the last bytes left the socket. This needed a response too large for the kernel's socket buffer and a client that was slow to read it.
  • Fixed: a node:http response never emitted 'drain' when res.write() had returned false and other code, such as a heartbeat timer, called res.write() again just as the client caught up. A stream.pipe(res) on that response then hung.
  • Fixed: a node:http server never answered a pipelined request when part of its body arrived after the previous response had ended.
  • Fixed: server.close() on a node:http server called back once in-flight responses finished, while their keep-alive connections were still open and serving requests. closeAllConnections() after close() did nothing, so an open connection kept the process alive.
  • Fixed: server.closeAllConnections() also stopped the listener and destroyed tunnels and WebSockets.
  • Fixed: writes to a node:http 'upgrade' or 'connect' socket stayed buffered until socket.end() when the same keep-alive connection had already served a request. The npm ws package stalled this way. Fresh connections were unaffected.
  • Fixed: CONNECT and Upgrade tunnel sockets kept reading while paused.
  • Fixed: a node:http 'connect' or 'upgrade' listener that called socket.end() missed part of the data the client had sent right behind the request. This needed request headers that arrived in more than one read, with more than 17 KB of data behind them.
  • Fixed: a node:http server dropped the body of a HEAD or TRACE request that declared one with Content-Length.
  • Fixed: Proxy-Connection: close now closes the connection, like Connection: close.
  • Fixed: HTTP/1.0 requests with an Expect header get a plain 'request', not 100 Continue.
  • Fixed: servers fed a socket through server.emit("connection", duplex) or http2's allowHTTP1 fallback threw ERR_HTTP_SOCKET_ASSIGNED on pipelined or fast keep-alive requests.
  • Fixed: req.setTimeout() never fired when a pipelined request's body stalled behind a pending response. The server closed the socket instead.
  • Fixed: node:http server.close() never called back when a request body finished arriving while the IncomingMessage was paused, the response ended later, and the same keep-alive connection then served another request.
  • Fixed: calling listen() on a node:http server that was already listening leaked the first listener and kept the process alive after close(). It now throws ERR_SERVER_ALREADY_LISTEN, like Node.
  • Fixed: node:http servers emitted 'clientError' repeatedly for the same stalled request (eventually triggering MaxListenersExceededWarning) when the listener didn't destroy the socket after a headersTimeout or requestTimeout.
  • Fixed: in node:http servers, req.socket emitted only 'close' when the client closed the connection, never 'end' for a FIN or 'error' (ECONNRESET) for a reset.
  • Fixed: req.socket.setKeepAlive() and req.socket.resetAndDestroy() in a node:http server returned undefined instead of the socket, so chaining threw.
  • Fixed: a node:http server that called res.detachSocket() on an unfinished response could, in rare cases, crash on a later use of that response, or never exit, after the client disconnected. Regression since v1.4.0.
  • Fixed: node:http and node:https proxy errors (ERR_PROXY_TUNNEL, ERR_PROXY_INVALID_CONFIG) included the username and password from the proxy URL.
  • Fixed: a proxied node:https request (new https.Agent({ proxyEnv }), NODE_USE_ENV_PROXY=1) emitted 'close' but never 'error' (ECONNRESET) when the connection ended after the proxy accepted CONNECT but before the TLS handshake finished.

node:http2

  • Fixed: on Linux, http2.connect() to a closed port reported ECONNRESET instead of ECONNREFUSED. With no 'error' listener, the failure was silently swallowed. Regression since v1.4.0.
  • Fixed: a node:http2 request with [http2.sensitiveHeaders] also sent a literal nodejs.http2.sensitiveheaders header that listed those header names. Bun.spawn env and macros also turned Symbol keys into string keys.
  • Fixed: node:http2 ALTSVC and ORIGIN frames used UTF-8 instead of latin-1, so a value containing bytes 0x80 to 0xFF read differently on a Node.js peer. ASCII values were unaffected.
  • Fixed: http2.connect(), createServer() and createSecureServer() accepted invalid options that Node rejects, such as a non-boolean strictSingleValueFields.
  • Fixed: ClientHttp2Stream.close(code) emitted error for NGHTTP2_CANCEL, and end before error for other error codes.
  • Fixed: session.originSet threw after destroy() on a TLS session that had never read it. It now returns undefined, like Node.

node:tls

  • Fixed: tls.Server#setSecureContext() had no effect on a server that was already listening.
  • Fixed: tls.Server#addContext() and Bun.serve({ tls: [{ serverName }] }) served the default certificate for a hostname with an unusually large number of labels. An addContext() wildcard also did not cover the hostname passed to listen().
  • Fixed: client-side new tls.TLSSocket(socket) (STARTTLS) never started a handshake. write() threw a TypeError and _start() threw ERR_MISSING_ARGS. The mysql package connects this way when ssl is set.
  • Fixed: after tls.connect({ socket }), 'data' listeners left on the plain socket kept firing with the encrypted bytes. A STARTTLS listener that called tls.connect({ socket }) on every chunk got Invalid socket from the second call.
  • Fixed: a TLS socket that wraps a net.Socket ignored the wrapped socket's allowHalfOpen. A STARTTLS server created with net.createServer({ allowHalfOpen: true }) could not reply after the client ended its side.
  • Fixed: a net.Socket wrapped by new tls.TLSSocket(socket) or tls.connect({ socket }) emitted its own 'error' on a peer reset, and 'end' and 'finish' on tlsSocket.destroy(err), on top of the TLS socket's events. Node emits only 'close' on the wrapped socket.
  • Fixed: destroying a tls.connect({ socket }) client in the same tick it was created left the wrapped net.Socket open, with no FIN sent, if that socket still had unflushed writes.
  • Fixed: when a node:tls socket ran over a plain stream.Duplex (not a net.Socket), destroying the Duplex with an error threw an uncaught exception instead of emitting 'error' on the TLS socket. An https.request over such a socket exited the process.
  • Fixed: a server-side tls.TLSSocket over a Duplex emitted ECONNRESET and no 'close' when the Duplex was destroyed before the handshake finished. If it was destroyed in the same tick as the wrap, the TLS socket emitted nothing.
  • Fixed: in an uncommon configuration, a node:tls server over a Duplex whose ALPNCallback writes to the socket, the server could crash during the handshake.
  • Fixed: end() on a node:tls socket over a Duplex never ended the Duplex (the peer saw no FIN) if the peer stalled mid-handshake or never answered the TLS close.
  • Fixed: tls.TLSSocket end() and destroySoon() sent no FIN when the peer never answered the TLS handshake, so the socket never emitted 'close'.
  • Fixed: an accepted node:tls socket whose peer reset the connection during a large write could emit only 'end', never 'error' or 'close', so server.close() never completed. This was seen on Linux and depended on timing.
  • Fixed: a tls.connect() or node:https client that wrote before a TLS 1.3 handshake finished could get ECONNRESET instead of the reply. This needed a server without noDelay that reset the connection a few milliseconds after replying.

node:net and node:dns

  • Fixed: a node:net socket could drop or resend bytes after a partial writev(). This needed a peer that had stopped reading, data already buffered for it, and a new write that the kernel accepted only part of.
  • Fixed: net.Socket#write() returned true when the send failed immediately (ECONNRESET, EPIPE, or a closed handle). It now returns false and sets socket.errored, matching Node.
  • Fixed: node:net routed an exception thrown in a socket 'data' or server 'connection' listener to the socket's 'error' event and closed the socket, instead of uncaughtException like Node.
  • Fixed: a node:net or node:tls socket was never garbage collected if it was destroyed before its connection attempt started (in the same tick as connect(), or during the DNS lookup), or if its DNS lookup failed.
  • Fixed: new net.Socket({ fd, readable: false, writable: true }) ended and closed the fd one tick after construction, so later writes failed with EPIPE.
  • Fixed: fetch(), Bun.connect() and dns.lookup() (on macOS and Windows, or with the system/libc backend) reported a temporary DNS failure (SERVFAIL or every nameserver timing out) as ETIMEOUT instead of EAI_AGAIN, so retry logic keyed on EAI_AGAIN never fired.

node:child_process

  • Fixed: on Linux and macOS, with child_process.spawn() and an "ipc" stdio slot, a non-JS child (e.g. Python) that wrote a message larger than the socket buffer in one write() got a short write, and the message never arrived.
  • Fixed: child_process.spawnSync() of a command that can't be spawned returned status: undefined, pid: undefined and output: [null, null, null]. It now returns status: null, pid: 0 and output: null, like Node.
  • Fixed: child.stdin.end(cb) and tty.WriteStream#end() called with no data fired cb and 'finish' while earlier writes still waited on a full pipe. A parent that exited in cb truncated the child's input. end(chunk, cb) was not affected.
  • Fixed: child_process.spawn() now throws ERR_INVALID_ARG_VALUE when stdio holds a tls.TLSSocket, like Node.js.
  • Fixed: calling destroy() inside a 'data' listener on child.stdout, child.stderr, or a Readable.fromWeb() stream emitted one more 'data' (and sometimes 'end'). exec() with maxBuffer could overshoot the limit by a chunk. Regression in v1.4.0.

node:module

  • Fixed: require() and require.resolve() crashed when Module._resolveFilename was set to a non-function. They now throw a TypeError, like Node.
  • Fixed: require() of an ES module crashed when code had replaced Module._resolveFilename with its own function, and that function returned a path that was not normalized (a symlink, or one containing /./, /../ or //).
  • Fixed: the second require() of an ES module threw if a Module._extensions handler had called the original loader and caught the module's error. The module was dropped from require.cache.
  • Fixed: Bun crashed when a preload set Module.runMain to a non-function such as {} or a string. An override that threw printed only Error occurred loading entry point: JSError, without the error.

node:vm

  • Fixed: when the host passed its File or fs.Stats class into a node:vm context, class Upload extends File {} there crashed on new Upload(...). A host PerformanceObserver whose callback was created in a context crashed when it delivered entries.
  • Fixed: vm.runInContext(), vm.runInNewContext() and the matching vm.Script methods crashed instead of throwing a TypeError when an option such as displayErrors was given a null-prototype object that had a custom util.inspect function.
  • Fixed: node:vm aborted the process, even inside try/catch, when text it built from JS, such as compileFunction() parameters or a thrown error's stack, passed the maximum string length of 2^31 - 1 characters.
  • Fixed: a vm.Script, vm.compileFunction or vm.SourceTextModule kept its context alive for the life of the process when its importModuleDynamically callback could reach that context (such as a closure over the sandbox) and the code left a function on it.
  • Fixed: in a node:vm context, the TypeError thrown when Object.defineProperty on the global failed was created in the host realm instead of the context's realm.

node:fs

  • Fixed: on Linux and macOS, node:fs opened files without O_CLOEXEC. Child processes forked by native code, such as node-pty or an addon calling system(), inherited those descriptors. Children of Bun.spawn and node:child_process did not.
  • Fixed: inside a node:fs callback, a microtask the callback queued ran before a process.nextTick() it queued. The order now matches Node.
  • Fixed: fs.readSync() and fs.read() threw ERR_OUT_OF_RANGE instead of Node's ERR_INVALID_ARG_TYPE when buffer was not a buffer and offset was invalid.

node:util

  • Fixed: util.format('%s', value) printed the util.inspect() output (such as <Buffer 61 62>) for Buffer, URL, and URLSearchParams. It now prints their toString() result, as Node does.
  • Fixed: every garbage-collected MIMEType from node:util leaked its type and subtype strings. That was about 57 bytes per instance for a text/html type with a charset parameter.
  • Fixed: util.aborted() invoked a user-replaced Function.prototype.bind on its internal abort listener.
  • Fixed: after a few hundred util.promisify() calls, util.promisify(setTimeout) could return the promise version of setImmediate or setInterval if that timer was promisified first, so await sleep(300) resolved at once. Regression in v1.4.1.

node:crypto

  • Fixed: in node:crypto, hash.update() after hash.end(), which throws in Node.js, hung forever on sha3-* hashes. hash.end() after digest(), as when pipe() ends a hash whose digest() was already called, emitted ERR_CRYPTO_HASH_FINALIZED instead of the digest.
  • Fixed: crypto.hkdfSync() and crypto.hkdf() returned an empty ArrayBuffer for a keylen of 0 instead of failing with "HKDF derivation failed", as Node.js does.

node:buffer

  • Fixed: buffer.transcode() aborted the process, even inside try/catch, when its output needed 2 GiB or more.
  • Fixed: buf.utf8Write(123), buf.hexWrite(123) and the other per-encoding Buffer write methods wrote the string form of a non-string value. They now throw ERR_INVALID_ARG_TYPE, like Node.js.

Native addons

  • Fixed: when the main thread or a Worker exited naturally, a native addon's N-API finalizers and cleanup hooks could still call into JavaScript, and that JavaScript could call a second addon that was already torn down. These calls now return napi_cannot_run_js, like Node.js.
  • Fixed: native addons that call v8::Function::GetScriptOrigin(), such as @newrelic/fn-inspect, failed to load on Linux and crashed on macOS.

Other modules

  • Fixed: node:wasi poll_oneoff threw TypeError: Invalid mix of BigInt and other type in subtraction on any clock wait with a positive timeout, so a WASI sleep() failed.
  • Fixed: node:wasi fd_pread reported twice the bytes read whenever a read filled its buffer.
  • Fixed: require("cluster") threw cluster._setupWorker is not a function when a script set process.env.NODE_UNIQUE_ID itself after node:net had loaded.
  • Fixed: node:dns and AsyncLocalStorage.bind() argument errors read The "undefined" argument must be of type ... instead of naming the argument.
  • Fixed: response bodies from an installed copy of Undici could stay pending instead of rejecting after a forced socket close, because node:stream's isReadable() returned null for web ReadableStreams.
  • Fixed: for await (const line of rl) over node:readline threw ERR_USE_AFTER_CLOSE and dropped the last line when the input ended with a single chunk of 1,026 or more lines. This hit Readable.from([text]) and HTTP responses, not files, stdin or child process output. Regression in v1.4.0.
  • Fixed: in node:quic, sendHeaders() on a stream opened before the handshake finished was not sent until the client wrote a body or ended the stream.
  • Fixed: url.parse() and url.resolve() crashed instead of rethrowing when they were passed an object instead of a string and a getter on that object (like constructor) threw a primitive value.
  • Fixed: class Sub extends StringDecoder {} returned the class itself from new Sub() instead of an instance, so write() was undefined.
  • Fixed: process.exit() called a replaced process.reallyExit with the wrong this (now process). It also threw a TypeError with an empty message when process.reallyExit was not a function.
  • Fixed: seven Node.js error codes had a wrong .message that now matches Node.js. For example, ERR_HTTP_TRAILER_INVALID read undefined and ERR_INVALID_URL_SCHEME read file.
  • Fixed: node-fetch's fetch(url, { body }) never settled when an old-style Stream body that isn't a Readable (such as form-data) emitted "error" before "end". It now rejects with that error.
  • Fixed: node-fetch's new Response(stream) threw for an old-style Stream that isn't a Readable. This broke node-fetch-cache.

Bun APIs#

  • Fixed: Bun.serve dropped WebSocket frames that a client sent without waiting for the 101 response, when they arrived in the same TCP read as the upgrade request. Browsers wait for the 101, so they were not affected.
  • Fixed: on Linux, a Bun.serve response serving a file of 1 MB or more could send file bytes before the end of its headers. This needed response headers too large for the kernel's send buffer (the repro used a 16 MB header).
  • Fixed: in a Bun.serve server with http2: true or http3: true, some requests with a streamed response body were never released, so pendingRequests stayed above zero and a graceful server.stop() never resolved. Both options are off by default.
  • Fixed: in Bun.serve with http3: true, calling server.stop() from inside a request handler spun at 100% CPU if that connection had already served a request. Calling stop(true) from a handler left clients waiting 10 to 30 seconds for their idle timeout.
  • Fixed: in Bun.serve with http2: true, the tail of a streamed body under 256 bytes was sent twice when the client's flow-control window cut it off more than once, causing PROTOCOL_ERROR.
  • Fixed: in Bun.serve({ http3: true }), a request header value that a client sent with leading or trailing whitespace reached the handler untrimmed. req.headers.get() returned " v\t" where HTTP/1.1 gave "v".
  • Fixed: with Bun.serve({ tls }), ws.terminate() waited for the peer's TLS shutdown reply instead of closing at once. If the peer had stopped reading, the close handler did not run until the idle timeout, and message() could still fire.
  • Fixed: Bun.serve crashed with a stack overflow when a handler called response.text() on a fetch() Response without awaiting it, then returned that Response before its body arrived. It now responds with a 500 via error().
  • Fixed: Bun.serve() responded 200 with an empty body when a second Response reused a ReadableStream an earlier response already sent. It now errors.
  • Fixed: Response.clone() on a body from an unread Bun.file().stream() dropped the MIME type, so Bun.serve sent no Content-Type for the clone.
  • Fixed: a Bun.serve handler that called req.clone() on a request with a body and then read neither copy leaked about 7 KB of memory per request.
  • Fixed: a Bun.serve response with a type: "direct" stream sent an empty body when pull() wrote and then called controller.close() in the same tick. cancel() also ran after every completed response.
  • Fixed: in a Bun.serve direct stream, a synchronous pull() that ended a chunked response (for example flush() then end()) and then threw in the same call wrote the last chunk twice. Strict clients then failed to parse the next keep-alive response.
  • Fixed: in a Bun.serve direct stream, an async pull() that called end() after an await and then never returned kept its request pending, so a graceful server.stop() never resolved.
  • Fixed: in a Bun.serve direct stream, a flush(true) promise never settled (and leaked) if end() was called while the socket was still backpressured.
  • Fixed: calling server.ref() after await server.stop() completed kept the process alive forever.
  • Fixed: Bun.spawn, Bun.spawnSync and node:child_process returned truncated data with no error when the kernel failed a read or write on a stdio pipe with an errno like EIO or ENOBUFS. They now report the error. Found by fault injection.
  • Fixed: on macOS and Linux, spawnSync or execFileSync could spin at 100% CPU and never return after a garbage collection during an earlier Bun.spawnSync freed a stream such as a previous test file's process.stderr. Users hit this in large bun test --isolate suites.
  • Fixed: after user code closed fd 0, 1 or 2 (e.g. fs.closeSync(1)), Bun.spawn could reuse that number for its own pipes. proc.stdout.text() then hung if fds 0 and 1 were both closed, and on Linux a child with stdout: "inherit" could overwrite a Blob of 8 MiB or more.
  • Fixed: on macOS, subprocess.signalCode and subprocess.kill() used Linux signal numbers, so a child killed by SIGBUS reported "SIGUSR1" and kill("SIGUSR1") sent SIGBUS. Signals numbered the same on both systems, like SIGTERM, SIGKILL and SIGINT, were not affected.
  • Fixed: on Linux, when the process hit its open file limit right after starting a child, Bun.spawn and child_process.spawn blocked until that child exited. A child waiting on a stdin pipe never did, which froze the process. They now fail with EMFILE.
  • Fixed: Bun.spawnSync crashed the process when it ran out of file descriptors (handles on Windows) while creating its event loop on the first call. It now throws EMFILE.
  • Fixed: on macOS and Linux, when a Bun.spawn({ terminal }) child exited with terminal.write() input still queued, the Bun.Terminal and its three pty file descriptors were never released and the process used CPU while idle. This was a regression in v1.4.0.
  • Fixed: on Windows, calling terminal.write() after the child exited leaked the Bun.Terminal and its callbacks, a regression in v1.4.0.
  • Fixed: on Linux and macOS, the process never exited when code held a reader on Bun.spawn stdout or Bun.stdin.stream(), stopped calling read() while more than 16 KB of output was still unread, and the other end then closed.
  • Fixed: a pipe-backed FileSink (Bun.stdout.writer(), Bun.spawn stdin) lost buffered data when a write was larger than the pipe could accept at once and end() was then called without a top-level await. The process exited before the reader drained the pipe.
  • Fixed: Bun.file().stream() hung instead of erroring when the underlying read() failed with an error like EIO, for example on /proc/self/mem. After such an error on a pty or non-blocking pipe, the process never exited.
  • Fixed: on macOS, await Bun.write(dest, Bun.file(src)) resolved to 0 instead of the byte count when dest already existed or was on another volume. The file itself was copied in full.
  • Fixed: Bun.file(path).exists() kept returning false after the file was created, if an earlier exists() or size on the same Bun.file() had found no file.
  • Fixed: an HTMLRewriter was never garbage collected when one of its handlers referenced the rewriter or the transformed Response.
  • Fixed: HTMLRewriter leaked the output Response when an element.onEndTag() callback referenced it and a handler threw or the output was cancelled before that end tag.
  • Fixed: when HTMLRewriter.transform() read a type: "direct" stream body and a handler threw, the output was cancelled, or the client disconnected, the stream's cancel() received undefined and its next write() threw. cancel() now receives the reason and write() returns 0.
  • Fixed: Bun.Image threw ERR_IMAGE_DECODE_FAILED for slightly damaged JPEGs (stray bytes, a missing end marker, truncated data) that libjpeg-turbo can decode with only a warning.
  • Fixed: in Bun.markdown with wikiLinks: true, a *, _ or ~ inside a [[target|label]] target paired with one outside the link, leaving an unclosed <em>, <strong> or <del>.
  • Fixed: in Bun.markdown, a * inside a reference link's label could leave an unclosed <em> when the link was followed by (< with no closing >, as in *[a*](<b with [a*]: /url defined.
  • Fixed: in Bun.markdown, a backslash before a space, tab or line ending in a link destination was treated as an escape, so [a](te\ st) rendered as a link instead of literal text.
  • Fixed: YAML.stringify() with an indent argument left a trailing space after keys, put empty [] and {} on their own line, and misaligned nested sequences at indent widths other than 2.
  • Fixed: YAML.stringify() could write a *alias in place of an unrelated object or array. This needed getters or Proxy traps that returned a new object on each read, and a garbage collection during the call.
  • Fixed: in a Worker, Bun.TOML.stringify() crashed instead of throwing RangeError: Maximum call stack size exceeded on an object nested about 9,500 levels deep, just under the depth limit. Deeper objects already threw.
  • Fixed: Bun.wrapAnsi() aborted the process instead of throwing a RangeError when an input line wrapped into tens of millions of rows or one row approached 2 GB.
  • Fixed: Bun.stripANSI() aborted the process on a non-Latin-1 string of 2^30 or more characters that contained an escape sequence, as did Bun.sliceAnsi() on about 78 million escape sequences in a row.
  • Fixed: Bun.indexOfLine() could miss a line break in input that is not valid UTF-8, so for await (const line of console) merged two lines.
  • Fixed: on Linux, Bun.Glob scans with *, ? or [...] could miss file names that are not valid UTF-8, such as names from a Latin-1 archive. bun pm pack could also pack such a file that .npmignore excludes.
  • Fixed: new Bun.FileSystemRouter() panicked or returned truncated route names for nested files when dir was an absolute path containing .., such as import.meta.dir + "/../pages".
  • Fixed: cc() from bun:ffi could crash or hang when several Workers made their first cc() call at the same time.
  • Fixed: subclassing Bun.Cookie, Bun.CookieMap, crypto.ECDH, crypto.DiffieHellman, dns.Resolver or $.Shell returned an instance of the base class, so instanceof and subclass methods broke.
  • Fixed: X509Certificate, crypto.hkdf, bun:sqlite, and Bun.CookieMap aborted the process when given a non-ASCII string of 2^30 or more characters. They now throw a RangeError or report no match.

Web APIs#

fetch

  • Fixed: on macOS, fetch(), sockets and dns.lookup() failed with ENOTFOUND for split-DNS names, like a host only a VPN's resolver knows while iCloud Private Relay is on.
  • Fixed: a fetch() body stream (res.body) or s3file.stream() that code created, never read and dropped kept its connection if the server stopped sending mid-body. With 256 of these open at once, later fetch() calls stayed pending.
  • Fixed: a regression in 1.4.1 where, after more than 64 concurrent fetch() requests to one keep-alive origin finished, the surplus idle connections were closed with an RST, so servers logged ECONNRESET. The responses themselves were not affected.
  • Fixed: a regression in 1.4.0 where fetch() leaked an ASCII string body when it rejected before sending, such as on an invalid header name or a GET with a body.
  • Fixed: fetch() sent the URL fragment to the server in the request line when the fragment contained a ? and no query string came before the #, as in /#/users?id=1.
  • Fixed: delete process.env.HTTPS_PROXY (or reassigning it after a delete) had no effect on later fetch() calls.
  • Fixed: fetch(url, { unix }) sent a proxy-form request down the Unix socket when HTTP_PROXY was set.
  • Fixed: a streamed fetch() response with Content-Encoding: br or zstd delivered only the first 4096 bytes of a larger flushed chunk, holding the rest until the next compressed chunk arrived.
  • Fixed: a compressed fetch() body read through a stream reader over HTTP/1.1 was decompressed as fast as it arrived, not as fast as it was read. With a slow reader and an unusually compressible body, memory could grow by hundreds of MB.
  • Fixed: fetch() rejected a valid Content-Encoding: deflate response with ZlibError when the server compressed with a zlib window smaller than 32 KB, or when the first read held only one byte of the body.
  • Fixed: after a fetch() was aborted mid-body, native readers of res.body such as new Response(res.body).text() or Bun.write() got a generic AbortError or an empty body instead of signal.reason.
  • Fixed: a stream taken from a fetch() body, req.body, S3File.stream() or HTMLRewriter output while the transfer was running, but first read after it had failed, ended cleanly with 0 bytes instead of rejecting. In rare cases since 1.4.0, a partly read stream crashed.
  • Fixed: a memory leak where a fetch() Response and its AbortSignal were never garbage collected if an abort listener on the signal referenced the response.
  • Fixed: after req.clone() in Bun.serve or res.clone() on a fetch() response, a reader on the original's body stream ended with done: true instead of rejecting when the body failed mid-stream. text() and the clone already rejected.
  • Fixed: arrayBuffer() and bytes() on a fetch() response or Response whose body exceeded 4 GiB aborted the process instead of rejecting with RangeError: Out of memory.
  • Fixed: over HTTP/2 and HTTP/3, fetch() kept leading and trailing whitespace on a response header value when the server sent it, which the HTTP/2 spec forbids. A padded Location header failed to redirect.
  • Fixed: fetch() with protocol: "http3" crashed when the QUIC connection closed before the response headers arrived and the automatic retry could not open a new connection at all, such as when no address for the host was reachable.
  • Fixed: aborting an HTTP/3 fetch() upload made the server treat the truncated body as complete. If the request declared a content-length, the server closed the connection and the next upload to that origin could reject with HTTP3StreamReset.
  • Fixed: HTTP/3 fetch() and Bun.serve({ http3: true }) spun at 100% CPU when the kernel kept refusing UDP sends, such as under a firewall DROP rule or a low egress MTU.

WebSocket

  • Fixed: the WebSocket client reported close code 1005 or a 1002 protocol error when the server's Close frame was split across two TCP reads within its first three bytes.
  • Fixed: a wss:// WebSocket through an HTTP CONNECT proxy reported close code 1006 "Failed to write" instead of the server's close code when the server ended TLS right behind its Close frame, as ws.close() in Bun.serve does.
  • Fixed: a wss:// WebSocket to an IP address such as 127.0.0.1, or through an HTTPS proxy at an IP address, closed with code 1006 when a TLS 1.2 server renegotiated (regression in 1.4.1).

Streams

  • Fixed: controller.write() in a type: "direct" ReadableStream never waited for a slow reader using getReader(), for await or pipeTo(). Once unread bytes reach highWaterMark (64 KiB by default), it now returns a promise that resolves on the next read.
  • Fixed: a direct stream's cancel(reason) was never called on reader.cancel(), stream.cancel(), or a break out of for await.
  • Fixed: in a direct stream, a write() made after end() or close() inside pull() was still delivered to the reader.
  • Fixed: bytes() and arrayBuffer() hung on a direct stream whose async pull() called controller.end() and never returned.
  • Fixed: Duplex.fromWeb dropped the last chunk of a type: "direct" ReadableStream when pull() called controller.error() or threw right after flushing that chunk. Only the error event was emitted.
  • Fixed: an async-iterable body (new Response(asyncGen()), a fetch upload, a Bun.serve response) was silently truncated or hung when the generator threw an error with code ERR_INVALID_THIS, instead of failing the stream.
  • Fixed: new Response(asyncIterable) kept pulling the iterator after its consumer had stopped early, through reader.cancel(), a break out of for await, a failed pipeTo(), or a Bun.write() that hit ENOSPC. It now calls the iterator's return(), so finally blocks run.
  • Fixed: finished(stream) from node:stream/promises never settled for a fetch() body or Bun.file() stream consumed natively (by Bun.write(), a fetch() request body, Bun.serve(), spawn stdin, or S3). The stream stayed readable.
  • Fixed: for a Response body backed by a string, Blob, typed array or file, reading res.body and then calling arrayBuffer(), bytes(), blob(), json() or formData() left the stream readable, so finished(body) from node:stream/promises never settled.
  • Fixed: a res.body stream taken before await res.text() (or json(), blob(), etc.) was left unlocked afterwards and getReader() still worked, unlike undici and browsers.
  • Fixed: reading blob.slice(start, end).stream() with .text(), .bytes(), .json() or Bun.readableStreamToText() returned the whole parent Blob if the parent and the slice had already been garbage-collected.
  • Fixed: ReadableStream cancel(), pipeTo() and pipeThrough() on a locked stream threw a TypeError without code: "ERR_INVALID_STATE", a regression from 1.3.
  • Fixed: web streams aborted the process instead of throwing RangeError: Out of memory when a queue or buffer hit its size limit, such as 67 million chunks queued and not yet read, or a single 2 GiB chunk (since 1.4.0).

Other

  • Fixed: postMessage() detached the ArrayBuffers in its transfer list even when it threw DataCloneError because a getter in the message closed a transferred MessagePort.

TLS#

  • Fixed: fetch(), WebSocket, and Bun.SQL with sslmode=verify-full failed certificate verification when connecting to an IPv6 literal like https://[::1]:3000, even when the cert had a matching IP SAN. fetch() rejected with ERR_TLS_CERT_ALTNAME_INVALID.
  • Fixed: a Bun.connect TLS socket that called shutdown() mid-handshake never fired close after end() while the peer stayed connected. The next socket to do the same never got its handshake callback.
  • Fixed: on a Bun.listen() TLS server, socket.pause() did not hold for a connection whose handshake was queued because many handshakes arrived at once. handshake and data still fired on it.

Runtime#

  • Fixed: on Linux and macOS, after a script accessed a piped process.stdout or process.stderr, a full pipe made console.log() and console.error() silently drop the rest of their output, and made writes in stdio: "inherit" children fail with EAGAIN.
  • Fixed: on Linux and macOS, an un-awaited Bun.write(Bun.stdout, new Response(stream)) to a full pipe truncated the output and exited with code 0 when nothing else kept the event loop alive.
  • Fixed: on Linux and macOS, a read from process.stdin or a child process's stdout that failed right after returning data (for example ECONNRESET) could end the stream with 'end' instead of 'error'.
  • Fixed: a promise that fetch(), Bun.write(), server.fetch() or Bun.resolve() returned already rejected (for example fetch("http://[bad")) never emitted unhandledRejection when nothing awaited or caught it, so the process exited 0.
  • Fixed: promise callbacks queued from a beforeExit listener (for example the code after an await) never ran before exit. This needed a script that had not yet used process.nextTick or loaded node:stream.
  • Fixed: drainMicrotasks() from bun:jsc also ran queued I/O and postMessage callbacks, nesting them inside the calling callback. It now drains only microtasks and process.nextTick.
  • Fixed: in JIT-optimized functions, a using dispose method that threw after the block body threw reported its own error instead of a SuppressedError carrying both.
  • Fixed: error.stack could show Error without the name and message, and drop new from constructor frames, if garbage collection ran before the stack was first read. It was reported for errors thrown by an async function and caught after await.
  • Fixed: calling the default Error.prepareStackTrace from a custom one (as source-map-support and depd do) headed every stack with Error instead of the error's name, like TypeError.
  • Fixed: in non-strict CommonJS code, calling the function that CallSite.getFunction() gave a custom Error.prepareStackTrace crashed for an async function frame after await or a generator frame after next(). It now returns undefined for internal frames.
  • Fixed: the first read of error.stack crashed when a custom Error.prepareStackTrace ran a synchronous GC (Bun.gc(true), a heap snapshot) and the strict-mode function that created the error was already unreachable. Fuzzing found it, and no user reported it.
  • Fixed: an error message that embedded a string near the maximum string length, as in Buffer.from("x", "q".repeat(2 ** 31 - 10)), aborted the process. It now throws a catchable RangeError. .stack on an error with a message that long now returns name: message without the frames.
  • Fixed: printing an error whose code was a String object with a throwing toString or no prototype crashed with "Bun has run out of memory".
  • Fixed: a crash when remapping a stack trace if a runtime transpiler cache file had been damaged on disk (by another process or a torn write) and its sourcemap header was invalid. Bun now discards and regenerates the bad entry.
  • Fixed: console.log and Bun.inspect ignored the depth limit for nested Map, Set, Array and Error cause chains and printed every level. A 1000-deep Map printed 2 MB, and nesting thousands of levels deep threw RangeError.
  • Fixed: a rare crash when garbage collection ran while a module that threw during evaluation (e.g. via import()) or a failed Bun.resolve() was being rejected.
  • Fixed: delete require.cache[path] of an ES module that was still loading, followed by import() or require() of it, crashed or evaluated the module twice. This needed a CommonJS dependency of that module to delete and re-import it while it loaded.
  • Fixed: import() crashed if mock.module(), a plugin's build.module(), or a bun --hot reload had replaced the module while an earlier import() of it was still loading dependencies.
  • Fixed: a runtime plugin's onResolve could run up to three times for one import or require(), the extra times on its own answer. It now runs once, when that line executes.
  • Fixed: require() of a path that a plugin's onResolve put in a namespace failed with Cannot find package. It now loads through the plugin's onLoad, like import.
  • Fixed: a plugin whose onResolve maps a to b and b to c loaded c for a. It now loads b.
  • Fixed: a require() in a branch that never ran still called a plugin's onResolve.
  • Fixed: a plugin's onResolve that threw failed the whole file. It can now be caught by a try around the require().
  • Fixed: import() resolved a relative path from a plugin's onResolve against the working directory instead of the importing module.
  • Fixed: each garbage-collected module leaked the string that held its import.meta URL. Re-importing after delete require.cache[...], hot reloading, or terminating Workers leaked one such string per module.
  • Fixed: on Linux, process.on("memoryPressure") emitted a false critical event shortly after a listener was added, with no real memory pressure. Hosts running a privileged PSI watcher such as systemd-oomd did not see it.
  • Fixed: on Linux without pidfd_open (an old kernel, or a seccomp filter that blocks it), a child process exiting could interrupt a system call on the main thread with EINTR. A native addon or bun:ffi call that doesn't retry could see a failed read().
  • Fixed: Bun crashed at startup when the working directory, a --config path, or $XDG_CONFIG_HOME/$HOME was so long that adding bunfig.toml passed the maximum path length (4096 bytes on Linux, 1024 on macOS).
  • Fixed: on macOS, bun --watch leaked one file descriptor for the project directory on every reload.
  • Fixed: bun --watch and bun --hot kept a file descriptor open for every directory the resolver read, reaching thousands in node_modules-heavy monorepos.
  • Fixed: after a reload followed by a garbage collection, bun --hot could crash, stop reloading on save, or report an already-handled rejection as the entry point's error. Which one, if any, depended on what the program allocated next.
  • Fixed: on Linux, a seccomp profile that fails faccessat2 with EPERM or EINVAL made a hoisted bun install from a warm cache fail with "package was not found in the cache". Regression in Bun v1.4.0.
  • Fixed: on Linux, a seccomp profile that denies prlimit64 made bun install, bun build and bun run <script> panic. Regression in Bun v1.4.0.

Transpiler#

  • Fixed: in a class with standard decorators, a field initializer that read a decorated field, like @dec a = 1; b = this.a, saw undefined.
  • Fixed: static { this.#m } in a class with an accessor field was a SyntaxError.
  • Fixed: two classes with standard decorators or accessor fields in the same scope could get each other's field initial values or throw "Cannot add the same private member more than once".
  • Fixed: in bun run, a static accessor initializer that read a let or const declared above the class threw a ReferenceError. This needed a top-level class statement with no other side effects.
  • Fixed: after a process transpiled about 2 GiB of modules at runtime (for example a require.cache-clearing loop), valid files could fail with a SyntaxError because keywords lost their trailing space.
  • Fixed: the JavaScript parser crashed on an import { ... } clause with more than 65535 names, affecting bun run, bun build, and Bun.Transpiler.
  • Fixed: Bun.build used quadratic memory on files that declare the same TypeScript enum many times. 8,192 declarations dropped from 8.6 GB to 1.1 GB.
  • Fixed: a + chain that repeated an inlined TypeScript string enum member thousands of times used quadratic memory. 8,192 terms took 1.4 GB in bun run.
  • Fixed: a TypeScript string enum member whose value was itself a concatenation, like B = "1" + "2", printed the wrong value after it was used in a template literal. A second use crashed the transpiler.
  • Fixed: a non-binding parameter in a TypeScript type literal signature, like type T = { foo(1): void }, reported error: Backtrack with no location instead of Unexpected 1.

bun install#

  • Fixed: bun install did not apply --cafile, --ca or the bunfig cafile/ca settings when it reached an https registry through HTTPS_PROXY, so a registry with a self-signed certificate failed with DEPTH_ZERO_SELF_SIGNED_CERT. Certificates were still verified.
  • Fixed: on Windows, bun install hung at 100% CPU and never saved the lockfile when the package it replaced held the last hard link of a running executable, as after --backend copyfile or a deleted --cache-dir. opencode upgrade hit this.
  • Fixed: bun install from an existing bun.lock into a new node_modules reinstalled an npm package's bundled file: dependency as self-referencing symlinks, so importing the package failed with Cannot find module. @fly/sprites was affected.
  • Fixed: a regression in Bun v1.4.0 where bun install rejected a root file: package's own relative file: dependencies that point outside the project.
  • Fixed: bun update <name> exited 0 and rewrote package.json when <name> was an optional dependency that resolved through an npm: alias and its download failed.
  • Fixed: bun update <dep> <peer> failed with error: <dep> failed to resolve when <peer> was an auto-installed peer dependency that nothing else depends on. Updating either name alone worked.
  • Fixed: bun install and bun pm migrate panicked migrating a yarn.lock whose resolved tarball URL had /-/ right after the host, such as the tarball of the npm package named -.
  • Fixed: bun info and bun pm view panicked when the registry returned a version entry that is not an object. They now print a parse error.
  • Fixed: bun pm ls --all and bun list --all panicked with buffer too small when a dependency's resolved spec, such as a tarball URL, was longer than 512 bytes.
  • Fixed: a regression in Bun v1.4.0 where bun pm pack and bun publish panicked with int cast when a write to the tarball failed, for example on a full disk. They now print the error, such as ENOSPC, and exit 1.

JavaScript bundler#

  • Fixed: a regression in 1.4.1 where a bundled const { v } = require("./b.js") of an ES module saw a later value of v. This needed the require() to run while b.js was still initializing, through an import cycle or a callback.
  • Fixed: in bundled output, Object.defineProperty, delete, and Object.freeze on the default import of a CommonJS module did not affect later property reads (regression in 1.4.1).
  • Fixed: a regression in 1.4.1 where, with --splitting, a lazy chunk could import an entry without [hash] in its name, so loading that entry with a query string (index.js?v=1) ran it twice.
  • Fixed: with --splitting and --target=bun, a require() of an ES module that imports back into its caller could read a binding before it was initialized, when small chunks were merged.
  • Fixed: with --splitting, a chunk shared by several entry points could print an import cycle in the wrong order, so a hoisted var read as undefined at runtime. opencode hit this when built with Bun 1.4.1 or 1.4.2.
  • Fixed: in bun build output, a barrel file's export * as ns from "./b" was undefined when a file the barrel re-exported earlier imported ns back from the barrel and used it at load time.
  • Fixed: without --splitting, bun build emitted __INVALID__REF__ for an import()ed module whose only top-level code, besides function declarations and constants, was a dead await (like false && await 0) or, with --target=bun, a using with a constant initializer (like await using x = null).
  • Fixed: bun build loaded a file's type-only and macro imports when another module imported that file with import * as or export * from. The build failed only if one of them could not be bundled, like a macro module importing bun under --target=browser.
  • Fixed: tsconfig jsxImportSource: "solid-js" emitted React.createElement calls instead of using the automatic runtime. jsx = "solid" and --jsx-runtime=solid are now errors.
  • Fixed: Bun.build({ tsconfig: "./custom.json" }) ignored the option and used the tsconfig.json that Bun found by itself.
  • Fixed: bun build and bun run printed Internal error: directory mismatch for directory whenever --tsconfig-override was passed. The override still applied and the command still succeeded.
  • Fixed: in a non-minified bundle, a binding named NaN, Infinity, or undefined could capture an inlined constant such as a macro returning NaN, changing its value.
  • Fixed: after Bun.build finished, all but one bundler worker thread kept its memory (about 100 MB extra after bundling a 2.5 MB file) until its next task.
  • Fixed: outputs[..].bytes from bun build --metafile and Bun.build({ metafile: true }) undercounted chunks that import other outputs, have a source map comment, or are HTML.
  • Fixed: with --splitting, --metafile-md reported an import() of a bundled file as an external import and counted it under "External imports".
  • Fixed: Bun.build({ files }) crashed when an in-memory file contained an import or url() specifier longer than the OS path limit, such as an inline data: image over 4 KB in CSS.
  • Fixed: bun build crashed on a malformed data URL with no comma, such as url(data:) in CSS, href="data:" in HTML, or import "data:".
  • Fixed: bun build with the default --target=browser panicked when a relative import such as import "../.." resolved to the filesystem root.
  • Fixed: bun build --sourcemap sometimes crashed when linking failed, for example on No matching export. It now prints the error. Bun.build() was not affected.
  • Fixed: Bun.Transpiler and bun build --no-bundle crashed with identifier minification on an empty file or a JSON, TOML, YAML or text loader input.
  • Fixed: Bun.Transpiler and bun build --no-bundle printed a leading space when output began with ++x or +x. They also wrapped a leading let; in parentheses.
  • Fixed: the dev server and bun build --react-fast-refresh crashed on a .tsx file where a class method, accessor or constructor calls anything named like a hook, such as app.useGlobalPipes().
  • Fixed: with React Fast Refresh, a component kept its state after an edit renamed the variables a hook is assigned to, such as const [a, setA] = useState(0) to const [b, setB] = useState(0). The state now resets, matching react-refresh/babel.
  • Fixed: the dev server could crash when a client sent an HMR WebSocket message that only Bun's own test suite uses. The HMR client that runs in the browser never sends it.
  • Fixed: the dev server panicked on an HTML route's first bundle when two files imported a file that itself had an unresolved import, such as a package that isn't installed.

bun build --react-compiler

  • Fixed: with --target=browser, the build crashed on a component where a closure read a let that was declared below the closure and reassigned later. With some closure bodies the component was left unmemoized instead.
  • Fixed: the build crashed on a component containing an object literal with a method shorthand like { m() {} }. This needed --target=bun, --target=node, or ssr output mode.
  • Fixed: with --target=browser, the build panicked with "Expected a node for all scopes" when a ternary's test assigned a call result, like (m = f()) ? 1 : 0, and m was later put in an array or object literal. That function is now left uncompiled.
  • Fixed: build memory grew quadratically with the size of one component. A 100-term || chain used 80 MB, 400 terms about 1 GB, and 800 terms was OOM-killed at 3 GB. 800 terms now uses 78 MB.
  • Fixed: build memory doubled with each statement like if (a) { log() } else if (b) { v = 1 } that reassigned the same local in one component. 16 of them cost 4 MB, 22 cost 1 GB, and more than 24 aborted.
  • Fixed: compile time doubled with each level of function expressions nested inside one component. 20 levels took 0.18 s, 25 took 5.3 s, and 30 never finished.
  • Fixed: a member assignment whose right-hand side reassigns a variable used in its target, like tail.next = tail = node or arr[i] = i++, could throw a TypeError or store to the wrong object or index.
  • Fixed: a compound assignment to a local inside a larger expression ran before that expression's earlier reads of the local, so o["k" + i] = i += 2 wrote the wrong key. i += 2 as its own statement was not affected.
  • Fixed: two assignments of constants to the same variable inside one expression could be reordered when one value was constant-folded, so [(x = 5), -(x = 10), x++] returned wrong values.
  • Fixed: a component or hook that called require() or import() inside its own body and kept the result in a local, like const { a } = require("./x"), threw TypeError or ReferenceError when it ran (regression in 1.4.1). A module-level require() was not affected.
  • Fixed: without --minify-identifiers, a compiled component's local shadowed a bundled module's export of the same name that it read through an alias or namespace. import { theme as defaultTheme } then const theme = custom ?? defaultTheme threw ReferenceError.
  • Fixed: assigning a value from props to a let variable inside a JSX prop or call argument, like v={(x = props.n)}, threw ReferenceError: t0 is not defined when the component read x afterwards.
  • Fixed: a never-read local assigned at the end of several catch handlers lost its let, causing ReferenceError: v is not defined.
  • Fixed: with the classic JSX runtime, the output called jsxDEV() without importing it, so it threw ReferenceError: jsxDEV is not defined on first render.
  • Fixed: JSX tags starting with _ or $ (such as the _Trans binding from Lingui's <Trans> macro) compiled to string tags, so the component never rendered.
  • Fixed: a children attribute alongside JSX children (<div children="x">y</div>) was merged into an array instead of being overridden.

bun build --compile#

  • Fixed: on Linux, bun build --compile run from inside a compiled executable (BUN_BE_BUN=1 or Bun.build({ compile })) wrote an executable that crashed at startup, a regression since v1.3.14.
  • Fixed: bun build --compile --bytecode --format=esm --splitting wrote an executable that differed by a few bytes on every run from the same inputs, breaking reproducible builds. The executables ran the same.
  • Fixed: with bytecode: true, Bun.build returned an undefined output and dropped the last asset when a chunk could not be compiled to bytecode, such as one with an invalid \p{...} regex.
  • Fixed: a --compile --bytecode executable that had called inspector.open() crashed when a debugger client connected.

JavaScript minifier#

  • Fixed: a Bun v1.4.1 regression where bun build --minify-syntax (or --minify) output threw ReferenceError: m is not defined. It needed const m = await import("./x") inside a function, and a next statement that used m itself once before reading m.n, like return [m, m.n].
  • Fixed: the minifier rewrote new Array(n, ...rest) to [n, ...rest], so an empty rest gave [n] instead of n empty slots (also under bun run).
  • Fixed: with --minify-whitespace, a keyword directly before require() lost its space (return__toCommonJS(...), returnglobalThis.Bun), so the bundle threw ReferenceError or failed to parse. It needed require("bun") with --target=bun, or a bundled ES module whose top level held only functions, classes and primitive constants.
  • Fixed: with --minify-syntax, a computed template tag like ns["tag"]`x` on a CommonJS export lost its this, throwing TypeError when the tag used this.
  • Fixed: bun build --no-bundle and Bun.Transpiler with identifier minification could rename a parameter, local or import to the name of an export when that name was short (such as t), so the output threw TypeError or failed to parse. Bundled output was not affected.

CSS Parser#

  • Fixed: the CSS parser rejected a ::view-transition-* name plus classes or a chain of classes, like ::view-transition-group(hero.big) or ::view-transition-new(.a.b), with Unexpected token: .
  • Fixed: ::view-transition-group-children() printed an "Unsupported pseudo-element" warning, and in CSS modules its name or class argument was not scoped.
  • Fixed: in CSS modules, classes inside ::view-transition-group(), ::view-transition-old(), ::view-transition-new() and ::view-transition-image-pair() were hashed but missing from the exports object.
  • Fixed: bun build dropped an explicit border-box clip from the CSS background shorthand when the origin was content-box, turning red content-box border-box into red content-box.
  • Fixed: bun build could drop a CSS rule that had nested rules. This needed the same selector four times in one file: two plain rules that merged into the rule before them, the rule with nesting, then a plain rule repeating its properties.

bun test#

  • Fixed: mock.module() spun at 100% CPU forever when its factory returned a pending promise for a module that was already imported, as in the vitest partial-mock pattern.
  • Fixed: with jest.useFakeTimers(), advanceTimersByTime() and runOnlyPendingTimers() never returned when an abort listener armed a new AbortSignal.timeout(0) every time it fired, or when code polled with while (!done) await Bun.sleep(0).
  • Fixed: with bun test --isolate or --parallel, a promise or callback that a finished test file left in flight could run during the next file and leak its timers, servers, or process.chdir() into it.
  • Fixed: with bun test --isolate, a FinalizationRegistry callback from a finished file could run during a later file if a garbage collection landed just as the first file ended. If that callback threw, the later file lost its remaining tests.
  • Fixed: with bun test --isolate or --parallel, a static import that a runtime plugin's onResolve put in a namespace never reached the plugin's onLoad. This happened when the resolved path alone no longer matched the onResolve filter, as with ./data.bar?custom or virt:thing.
  • Fixed: bun test --parallel exited 130 on SIGTERM instead of 143, and a worker that crashed mid-file left processes its test spawned running after the run.
  • Fixed: a crash when a node:vm context, or a finished test file's global under bun test --isolate or --parallel, was garbage collected while one of its FinalizationRegistry callbacks was still queued.
  • Fixed: a crash in bun test --isolate and --parallel when a finished test file left a Bun.SQL or Bun.RedisClient open and retried from its rejected query or onclose handler, once the retry's connectionTimeout fired.
  • Fixed: bun test could crash with NAPI FATAL ERROR after all tests passed when a native addon such as sqlite3 still had work in flight.
  • Fixed: bun test --coverage counted only the last load of a file loaded more than once, such as import("./lib.ts?a") then ?b, so lines only an earlier load ran showed as uncovered. Two overlapping import()s of one file dropped it from the report.
  • Fixed: spyOn(obj, 0) on a non-function indexed property returned the mock on reads instead of the value and crashed on the next write.
  • Fixed: in bun test, an assertion that compared an asymmetric matcher like expect.any(String) or expect.stringContaining() with an array hole crashed instead of failing. Inside expect.arrayContaining(), so did a matcher at an index past the end of the actual array.
  • Fixed: expect.not.any(), expect.resolvesTo.any() and expect.rejectsTo.any() gave the wrong verdict for primitive values, so expect(5).toEqual(expect.not.any(Number)) passed.
  • Fixed: in bun test, a snapshot matcher or a failing toEqual() crashed when it had to print a value nested many thousands of levels deep. It now throws a RangeError.

Bun Shell#

  • Fixed: in Bun Shell, a pipeline command that failed with a JavaScript error before it started, such as a redirect into a Response (cmd | cat > ${new Response("r")}), left the other commands running and the process never exited.
  • Fixed: a Bun Shell command with < ${buffer} stdin never finished when the buffer was larger than the pipe buffer, the command exited without reading it, and a background process it had started still held stdin open.
  • Fixed: Bun Shell's .then() and .catch() threw synchronously instead of returning a rejected promise when the shell failed to start, such as .cwd() to a missing directory. Code that used await saw an ordinary rejection either way.

SQL / SQLite / S3 clients#

  • Fixed: in Bun.SQL (Postgres), binding a number[], a plain object or a Date to a bytea parameter silently stored an empty bytea. Strings, Buffers and TypedArrays were stored correctly. It now rejects with a TypeError.
  • Fixed: in Bun.SQL (Postgres), a parameter that threw while it was encoded, such as an object whose toString() throws or { a: 1n } bound to jsonb, closed the connection, failing its pipelined queries and open transaction. Now only that query rejects.
  • Fixed: in Bun.SQL (Postgres), a result value the client could not decode, such as a multidimensional array, closed the connection and rejected every other pipelined query on it. Now only that query rejects.
  • Fixed: in Bun.SQL with MySQL, a query that failed client-side (e.g. a bad parameter) while it waited in a connection's queue never settled, and the next query queued on that connection resolved with another query's rows.
  • Fixed: in Bun.SQL (Postgres), right after a pipelined query failed, a statement new to that connection and the query issued with it could resolve with each other's rows. This depended on timing, except behind pgdog, where a syntax error triggered it every time.
  • Fixed: in Bun.SQL (Postgres), sql.begin() resolved even though the server rolled the transaction back, when the callback swallowed a failed query's error. It now rejects with ERR_POSTGRES_COMMIT_ROLLED_BACK.
  • Fixed: in Bun.SQL (MariaDB), a JSON column holding text that MariaDB accepts but JSON.parse refuses closed the connection and rejected every queued query. Now only the query that read it rejects.
  • Fixed: Bun.SQL (Postgres) returned a zero from a numeric(p, s) column as "0" instead of "0.0000" when the query used the binary protocol (prepared statements).
  • Fixed: bun:sqlite statements with more than 65535 parameters threw a wrong "expected N values" error, which broke large Bun.SQL sqlite bulk inserts (one report: 7389 rows of 11 columns). With an object of named parameters, the extra ones were silently bound as NULL.
  • Fixed: bun:sqlite bound a TypedArray with a detached ArrayBuffer as NULL instead of an empty BLOB.
  • Fixed: bun:sqlite bound a 2 GiB Uint8Array as empty text instead of throwing SQLITE_TOOBIG.
  • Fixed: Database.deserialize() in bun:sqlite threw for an ArrayBuffer, which its types and docs accept.
  • Fixed: Database.setCustomSQLite() threw SQLite already loaded in a Worker when called with the same path the process had already loaded.
  • Fixed: Bun.write(s3file, Bun.file(path)) and s3file.writer() held the entire payload in memory during a multipart upload, so a 500 MB upload raised peak RSS by about 850 MB.
  • Fixed: an S3File.writer() garbage-collected without end() leaked its buffered bytes, left the multipart upload open, and kept the process from exiting.

TypeScript types#

  • Fixed: the TextEncoder.encodeInto() types marked both arguments optional, so calls missing an argument type-checked but threw at runtime.
  • Fixed: @types/bun rejected code that runs fine, including test.todo("label") with no callback, test("label", { retry: 1 }, fn), expect(await sql`select 1`).toEqual(rows), Bun.YAML.parse(buffer), and new Blob().slice() without lib.dom.
  • Fixed: @types/bun failed to type check in projects installed with linker = "isolated" and globalStore = true. tsc reported TS2307: Cannot find module 'undici-types' when skipLibCheck was off.

Windows#

  • Fixed: on Windows 10 and 11, process.report.getReport().header.osRelease read 6.1 instead of 10.0.
  • Fixed: on Windows, Bun.connect() to a named pipe leaked a small amount of memory each time the connect failed synchronously, such as with an invalid tls certificate.