Bun v1.4.4
Fixes 271 issues, addressing 544 👍. bun fmt, a Prettier compatible formatter built into Bun that is 3x to 6x faster than oxfmt, bun check is 1.4x to 7.8x faster on large projects, bun install migrates Yarn Berry lockfiles, retries with exponential backoff and closes 133 issues, up to 3.2x faster hoisted installs from…

Bun v1.4.4

To install Bun
curl
curl -fsSL https://bun.sh/install | bash
npm
npm install -g bun
powershell
powershell -c "irm bun.sh/install.ps1|iex"
scoop
scoop install bun
brew
brew tap oven-sh/bun
brew install bun
docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun
To upgrade Bun
bun upgrade
bun fmt: a formatter built into Bun#
bun fmt
is an incredibly fast, Prettier-compatible code formatter that works with your existing Prettier or oxfmt config.
bun fmt
src/index.ts
src/styles.css
Formatted 2 files, 2 unchanged [5.59ms]
It formats JavaScript, TypeScript, JSX, Flow, CSS, SCSS, Less, HTML, Vue, Svelte, Angular, Lightning Web Components, MJML, JSON, JSONC, JSON5, YAML, TOML, Markdown, MDX, GraphQL and Handlebars. Embedded code is formatted too, like
<style>
in HTML, code blocks in Markdown and
css`...`
templates in JavaScript.
In Prettier's own repository (2,616 files, with their config),
bun fmt --check
takes 60 ms and Prettier takes 17 seconds:
| Tool | Time | Peak memory |
|---|---|---|
bun fmt | 59.7 ms | 48 MB |
| Prettier 3.9.10 | 17.1 s | 459 MB |
That's 286x faster. On vitest, date-fns, monkeytype and h3, it's 234x to 425x faster than Prettier 3.9.9.
On projects that already use oxfmt, with their own config, it's 3x to 6x faster than oxfmt 0.74.0:
| Project | Files | bun fmt | oxfmt 0.74.0 | Faster | Memory |
|---|---|---|---|---|---|
| RSSHub | 7,121 | 97.3 ms | 298.5 ms | 3.0x | 42 vs 95 MB |
| redocly-cli | 3,397 | 62.4 ms | 217.5 ms | 3.4x | 61 vs 107 MB |
| vite | 2,537 | 65.2 ms | 406.7 ms | 6.2x | 64 vs 137 MB |
| fumadocs | 2,452 | 44.6 ms | 1,395 ms | 31x | 48 vs 179 MB |
Both tools see the same files, exit 0 and leave the tree unchanged.
Reproduce these numbers
Each project is a fresh clone at a pinned commit. The oxfmt projects have no
node_modules
.
vite
bun install -g [email protected]
git clone https://github.com/vitejs/vite && cd vite
git checkout 4ca25e1d3a24
hyperfine --warmup 2 "oxfmt ." "bun fmt ."
redocly-cli
git clone https://github.com/Redocly/redocly-cli && cd redocly-cli
git checkout 82da67d6ea5b
hyperfine --warmup 3 --runs 20 "oxfmt --check ." "bun fmt --check ."
RSSHub
git clone https://github.com/DIYgod/RSSHub && cd RSSHub
git checkout 8776927c78de
hyperfine --warmup 3 --runs 20 "oxfmt --check ." "bun fmt --check ."
fumadocs
git clone https://github.com/fuma-nama/fumadocs && cd fumadocs
git checkout bc86bbb0d406
hyperfine --warmup 3 --runs 20 "oxfmt --check ." "bun fmt --check ."
prettier
git clone https://github.com/prettier/prettier && cd prettier
git checkout df39b5b60a28
bun install --ignore-scripts
hyperfine --warmup 3 "bun fmt --no-script . --check" "node node_modules/prettier/bin/prettier.cjs . --check"
The same runs on one core, and on only vite's JavaScript and TypeScript files (
'**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'
):
| Run | bun fmt | oxfmt 0.74.0 | Faster |
|---|---|---|---|
| vite, 1,608 JS and TS files | 52.2 ms | 188.3 ms | 3.6x |
| RSSHub, one core | 621.6 ms | 1,212 ms | 1.9x |
| redocly-cli, one core | 375.1 ms | 645.5 ms | 1.7x |
| fumadocs, one core | 255.2 ms | 5,608 ms | 21x |
- vite: 16-core M4 Max MacBook Pro, macOS 26.7. Mean.
- Everything else: Intel Xeon Platinum 8488C, 8 physical cores pinned with
taskset, Amazon Linux 2023, xfs on NVMe, warm file cache. Median.
bun fmt
wouldn't exist without Prettier, Biome and Oxc. It's a port of Prettier, and the JavaScript and TypeScript printer is a port of oxc_formatter, which is a port of Biome's formatter.
Thank you to the Prettier, Biome and Oxc teams for years of great work.
Read more in the docs.
bun fmt --check #
--check
lists the files that need formatting without writing anything. It exits with code 1 if there are any.
bun fmt --check
src/index.ts
src/styles.css
2 files need formatting. Run bun fmt to fix them.
bun fmt --staged and --changed#
--staged
only formats the files you're about to commit and re-stages them, so you don't need
lint-staged
.
.git/hooks/pre-commit
#!/bin/sh
bun fmt --staged
--changed
formats the files that differ from your default branch. Pass a ref to compare against something else.
bun fmt --changed
bun fmt --changed=origin/dev
Use your Prettier or oxfmt config#
bun fmt
reads
.prettierrc*
,
prettier.config.*
,
"prettier"
in
package.json
,
overrides
,
.editorconfig
and
.prettierignore
. It also reads
.oxfmtrc.json
,
.oxfmtrc.jsonc
and
oxfmt.config.ts
(or
.mts
). It accepts the same CLI flags as Prettier or oxfmt, and
// prettier-ignore
comments work.
"scripts": {
"format": "prettier --write ."
"format": "bun fmt"
}
Import sorting is built in, so you can leave
@trivago/prettier-plugin-sort-imports
,
@ianvs/prettier-plugin-sort-imports
and
prettier-plugin-organize-imports
in
plugins
. Languages that need a Prettier plugin, like Astro, fall back to the Prettier installed in your project.
The command is
bun fmt
, like
cargo fmt
,
go fmt
,
deno fmt
and
zig fmt
.
bun format
still runs your
format
script.
bun fmt --doctor
shows how Bun interprets your config and which packages you can uninstall.
Options in bunfig.toml#
All of Prettier's options are supported under the same names. Each one is also available as a flag, like
--print-width 100
or
--no-semi
.
bun fmt --init
creates the
[fmt]
section.
bunfig.toml
[fmt]
printWidth = 100
semi = false
sortImports = true
sortTailwindcss = true
sortPackageJson = true
jsdoc = true
[[fmt.overrides]]
files = ["*.test.ts"]
options = { printWidth = 120 }
sortImports
,
sortTailwindcss
,
sortPackageJson
and
jsdoc
are off by default, and don't need a Prettier or oxfmt config.
sortImports
groups imports, sorts the names in
{ }
and merges duplicate imports from the same module.
In a monorepo,
bun fmt
walks up from the working directory to the nearest
bunfig.toml
with a
[fmt]
section. Without a Prettier config,
bun fmt
also formats TOML. It skips
*.min.js
,
*.min.css
and lockfiles unless you pass them explicitly.
Prettier's Tailwind, package.json and Svelte plugins are built in#
bun fmt
does the work of
prettier-plugin-tailwindcss
,
prettier-plugin-packagejson
and
prettier-plugin-svelte
, so you don't need them installed. You can leave them in
plugins
, and their options are supported.
// Before
<button className="text-white px-4 sm:px-8 py-2 bg-sky-700 hover:bg-sky-800" />
// After
<button className="bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 sm:px-8" />
Tailwind classes are sorted with your project's
tailwindcss
, version 3 or 4. If it isn't installed, they're left as they are.
We compared the output against Prettier 3.9.9 with version 0.8.1 of the plugin, on Tailwind 4.3.3 and 3.4.19.
| Input | Total | Identical | % |
|---|---|---|---|
| JavaScript and TypeScript files | 13,952 | 13,654 | 97.9% |
| HTML, Vue, Angular, LWC, Handlebars, CSS, SCSS, Less, Markdown and MDX files | 2,176 | 2,148 | 98.7% |
| Tailwind 3 projects | 15 | 15 | 100% |
About half of the differences are intentional. In
className={`header${open ? " active" : ""}`}
, the plugin trims
" active"
to
"active"
, so the element ends up with the class
headeractive
.
bun fmt
keeps the space.
For
package.json
, keys are sorted in the same order as the plugin and values are left untouched. Of 13,670 real-world
package.json
files, 13,644 (99.8%) are byte-for-byte identical to the output of Prettier 3.9.9 with the plugin. 20 differ only in values that Bun preserves, and the plugin throws on the other 6.
.svelte
files are formatted by default, and the output matches
prettier-plugin-svelte
4.1.1. With a Prettier config, they're formatted if the config lists the plugin. To skip them, set
svelte = false
in
[fmt]
or pass
--no-svelte
.
If your config uses a plugin that
bun fmt
doesn't support, like
prettier-plugin-jsdoc
, it prints an error naming the plugin, skips the files the plugin applies to and exits with code 2.
Where bun fmt intentionally differs from Prettier#
Prettier's output changes the meaning of the code for these inputs.
bun fmt
preserves it.
| Input | Prettier 3.9.9 | bun fmt |
|---|---|---|
font-family: Font\ Awesome in CSS | Font\Awesome, where \A is a hex escape | Kept |
| `x | b` in a CSS selector | `x |
<!DOCTYPE html> in Handlebars | Dropped | Kept |
A <script> with a // comment in Handlebars | Lines joined, so the comment swallows the next line | Kept |
bun fmt --debug-check
re-parses each file after formatting and fails if the meaning changed, like Prettier's
--debug-check
.
Testing bun fmt#
Bun's CI runs Prettier 3.9.9's own formatting tests on every commit. A test passes when the output is byte-for-byte identical to Prettier's.
64 of the 83 differences are one intentional case: Prettier drops trailing blank lines from a YAML block scalar, which changes the string.
| Language | Tests | Identical | % |
|---|---|---|---|
| JavaScript | 1,357 | 1,356 | 99.9% |
| JSX | 186 | 186 | 100% |
| TypeScript | 756 | 756 | 100% |
| Flow | 1,505 | 1,503 | 99.9% |
| JSON | 785 | 785 | 100% |
| CSS | 162 | 160 | 98.8% |
| Less | 238 | 236 | 99.2% |
| SCSS | 136 | 135 | 99.3% |
| GraphQL | 104 | 102 | 98.1% |
| YAML | 1,621 | 1,554 | 95.9% |
| Markdown | 1,572 | 1,569 | 99.8% |
| MDX | 32 | 32 | 100% |
| HTML | 272 | 269 | 98.9% |
| Vue | 189 | 189 | 100% |
| Angular | 109 | 109 | 100% |
| Handlebars | 128 | 128 | 100% |
| MJML, LWC, misc | 579 | 579 | 100% |
| Total | 9,731 | 9,648 | 99.1% |
Another 90 tests are skipped. 66 of them use syntax proposals at stage 2 or below that only Babel parses, and 13 need a Prettier plugin.
With an oxfmt config,
bun fmt
matches oxfmt 0.72's output on 1,343 of oxfmt's 1,498 fixtures (89.7%).
On 330 open-source repositories, we ran each project's format command with its own formatter and with
bun fmt
. A run only counts as identical if every file is byte-for-byte the same. Next.js counts as a mismatch: 26,334 files are identical, but
bun fmt
rejects one file that uses a Babel-only proposal.
| Compared with | Runs | Identical | % |
|---|---|---|---|
| oxfmt 0.72 | 240 | 227 | 94.6% |
| Prettier | 74 | 57 | 77.0% |
Most of the Prettier mismatches come from an older version of Prettier, Prettier plugins or Markdown files.
New in bun check#
bun check is 1.4x to 7.8x faster on large projects#
Cold runs with 8 threads on a Linux x64 machine, best of 3. Both versions report the same number of errors on each project.
| Project | Bun v1.4.4 | Bun v1.4.3 | Faster |
|---|---|---|---|
MUI docs | 1.31 s | 10.28 s | 7.8x |
t3code server | 1.62 s | 4.59 s | 2.8x |
Supabase studio | 1.27 s | 2.84 s | 2.2x |
| mikro-orm | 1.82 s | 3.30 s | 1.8x |
| VS Code | 2.82 s | 4.09 s | 1.4x |
Peak memory on MUI's
docs
drops from 11.2 GB to 1.3 GB. The other four use up to 0.3 GB more.
Of 76 other repositories, 69 are more than 10% faster. Bun's own
src/js
is slower: 0.20 s, up from 0.11 s.
- A
switchwith thousands of cases, or a variable with thousands of assignments that's read from closures, no longer takes seconds to check. - On Windows, files load 31% to 45% faster in projects with an isolated install. That's 50 to 100 ms per run on opencode's packages.
bun check -b is up to 15x faster with many projects#
With
bun check -b
, projects share parsed declaration files, module resolution caches and the list of emitted files. Each project used to keep its own copy, so check time grew much faster than the number of projects.
bun check -b
on generated monorepos with
skipLibCheck: true
, best of 6 runs on a 16-core M4 Max:
| Monorepo | Bun v1.4.4 | Bun v1.4.3 | Faster |
|---|---|---|---|
| 1,000 independent projects, 5 files each | 0.53 s | 2.07 s | 3.9x |
| 4,000 independent projects, 5 files each | 0.87 s | 13.65 s | 15.7x |
| 800 projects with up to 40 references each, 20 files each | 1.26 s | 7.36 s | 5.8x |
In the last one, peak memory dropped from 1,987 MB to 872 MB.
New in bun install#
bun install migrates Yarn Berry lockfiles#
bun install
converts a Yarn 2+ (Berry)
yarn.lock
to
bun.lock
and preserves every pinned version. Bun v1.4.3 only migrated Yarn v1 lockfiles, so switching a Berry project to Bun re-resolved every dependency.
bun install
bun install v1.4.4
[59.59ms] migrated lockfile from yarn.lock
- Workspaces,
npm:aliases,resolutions, and git, tarball andfile:dependencies are preserved. patch:dependencies becomepatchedDependenciesinpackage.json.catalog:ranges are read from.yarnrc.yml. The catalogs are copied intopackage.json.
The migration is all-or-nothing. If Bun can't preserve everything
yarn.lock
pins, it prints the reason and falls back to resolving from
package.json
. For example:
- An
exec:dependency. - A registry set in
.yarnrc.ymlbut not inbunfig.tomlor.npmrc.
bun install retries with exponential backoff#
bun install
retries failed requests up to five times. In Bun v1.4.3, all five retries happened within 5 ms, so a brief registry outage would fail the install.
Now it waits 50 ms before the first retry and doubles the delay each time: 50, 100, 200, 400 and 800 ms, with ±20% jitter.
Installing one package from a misbehaving registry, and how long the command took:
| Registry | Bun v1.4.4 | Bun v1.4.3 |
|---|---|---|
| One 503, then OK | works, 78 ms | works, 22 ms |
| 503 for 100 ms | works, 222 ms | fails, 8 ms |
| 503 or 429 for 1 s | works, 1.6 s | fails, 7 ms |
| 503 for 3 s | fails, 1.5 s | fails, 18 ms |
| Nothing listening | fails, 1.6 s | fails, 15 ms |
To tolerate outages longer than 1.5 seconds, increase the initial delay:
# waits 250 ms, 500 ms, 1 s, 2 s and 4 s
BUN_CONFIG_HTTP_RETRY_BACKOFF=250 bun install
BUN_CONFIG_HTTP_RETRY_BACKOFF=0retries immediately, like before.Retry-Afterheaders are respected, up to 60 seconds.- 429 and 408 responses are retried.
133 bun install issues closed#
We had 597 open package manager pull requests. 464 landed in full and 40 landed partially.
Every one of these PRs had tests that failed in the previous version of Bun and now pass. This nearly doubled bun install's test suite.
We also fuzzed it:
- We compared the old and new versions on 600 generated projects covering every kind of dependency, both linkers and both lockfile formats.
- We ran 7 commands against 90,000 mutated
bun.lockbfiles without a crash or hang. - We compared version selection against
npm-pick-manifeston 200,000 generated cases. It differs in two: alatesttag that points to a missing version, and^1.0.0-a.0.
Hoisted installs from a warm cache are up to 3.2x faster on Linux#
The hoisted linker links packages on up to 8 threads on macOS and Linux. It helps most when the cache is warm and
node_modules
is missing or incomplete, like a CI job that restores Bun's cache.
Installing 1,000 packages with 30 files each on Linux, with everything already cached:
node_modules | CPU cores | After | Before |
|---|---|---|---|
| Empty | 8 | 120 ms | 381 ms |
| Empty | 4 | 174 ms | 383 ms |
| Empty | 2 | 162 ms | 222 ms |
| Half installed | 8 | 54 ms | 140 ms |
| Half installed | 4 | 65 ms | 174 ms |
| Half installed | 2 | 107 ms | 161 ms |
Linux installs skip native packages built for the other libc#
Packages like
sharp
,
@swc/core
,
lightningcss
and
rollup
publish one optional dependency per platform, and mark each Linux build with
"libc": ["glibc"]
or
"libc": ["musl"]
. Bun previously ignored that field, so every Linux install downloaded both builds. Now it only installs the one that matches your system, like npm and pnpm.
bun install --libc musl # for an Alpine image, from a glibc machine
bun install --libc '*' # both builds, like before
The field is saved in
bun.lock
. Lockfiles written by older versions of Bun don't have it, so existing packages pick it up when they're updated or when you regenerate the lockfile.
Peer dependencies resolve more like pnpm#
When nothing in your project provides a package's peer dependency, Bun installs a version that satisfies the peer range. Before, the package got whatever version an unrelated package happened to install.
package.json
{
"dependencies": {
"@modelcontextprotocol/sdk": "1.20.0",
"@remix-run/express": "2.17.1"
}
}
@remix-run/express
has a peer dependency on
express@^4.20.0
. The MCP SDK depends on
express@5
.
| Version | @remix-run/express gets |
|---|---|
| Bun v1.4.4 | express@4 |
| Bun v1.4.3 | express@5, with an incorrect peer dependency warning |
Installing
elysia
alongside
@jest/schemas
had the same problem:
elysia
got
@sinclair/[email protected]
and failed to load.
How
bun install
picks a peer dependency's version:
- If your project, the workspace or any ancestor of the package depends on the peer, that version is used, even if it's out of range. With
react@17in your app, every library it uses getsreact@17. - Otherwise, every package with that peer dependency shares a single version that satisfies all of them. Bun prefers a version that's already installed, then the newest one in the registry.
- If no single version satisfies all of them, Bun still avoids installing one copy per package, because packages like
reactandgraphqlbreak when two copies are loaded. Each package uses an installed version in its range if there is one, and otherwise shares the version the others use.
When an install has to resolve dependencies,
--verbose
prints every peer dependency that ended up out of range.
Versions already in
bun.lock
don't change. To re-resolve a peer, remove the package that needs it and add it back.
bun update --latest
only needs to run once. In Bun v1.4.3, updating
react-dom@17
to
react-dom@19
changed its peer range, but a
react
that was only installed as its peer stayed on 17 until the second run.
bun pm fetch #
bun pm fetch
downloads every dependency into the cache and nothing else. It doesn't create
node_modules
, run scripts or touch the lockfile.
In a Dockerfile, this keeps the downloads in a layer that's only rebuilt when
package.json
or
bun.lock
changes:
Dockerfile
COPY package.json bun.lock ./
RUN bun pm fetch
COPY . .
RUN bun install --offline --frozen-lockfile
In a monorepo, the first step only needs the root
package.json
and the lockfile.
bun pm cache pack, unpack and prune#
pack
writes the cache entries your lockfile needs into a single file, and
unpack
restores them on another machine.
# after an install
bun pm cache pack .ci/bun-cache.pack
# on another runner, before the install
bun pm cache unpack .ci/bun-cache.pack
bun install --frozen-lockfile
unpack
only adds what's missing, and rejects a corrupted pack entirely.
prune
removes old packages from the cache without clearing all of it:
bun pm cache prune # older than 30 days
bun pm cache prune --max-age 7 # older than 7 days
bun pm cache prune --dry-run # list what would go
bun publish --recursive and --filter#
Publish every package in a monorepo with one command. Dependencies are published before the packages that depend on them.
bun publish --recursive --quiet
skipped acme: private
skipped @acme/[email protected]: already on the registry
skipped @acme/docs: private
@acme/ui
Tag: latest
Access: default
Registry: https://registry.npmjs.org/
+ @acme/[email protected]
--filter "@acme/*"only publishes matching workspaces.- Private packages and versions already on the registry are skipped, so it's safe to re-run after a failure.
- Every package is packed before the first one is published.
packageExtensions #
Some packages import a dependency they never declare, which fails with isolated installs.
packageExtensions
in the root
package.json
adds the missing entry without a fork or a patch.
package.json
{
"packageExtensions": {
"legacy-plugin@2": {
"dependencies": { "lodash": "^4.17.21" }
},
"react-redux@7": {
"peerDependencies": { "react": "*" }
}
}
}
It uses the same format as pnpm and Yarn, and Bun copies the setting over when it migrates a
pnpm-lock.yaml
or
yarn.lock
.
link: supports paths#
link:./dir
symlinks a directory into
node_modules
, like in Yarn and pnpm. The path is relative to the
package.json
that declares it. You don't need to run
bun link
first.
package.json
{
"dependencies": {
"design-tokens": "link:./vendor/design-tokens",
"shared": "link:../shared"
}
}
link:shared
, without a path, still refers to a package registered with
bun link
.
bun install warns about deprecated packages#
bun add rimraf@3
warn: deprecated [email protected]: Rimraf versions prior to v4 are no longer supported
Deprecated direct dependencies each print one line.
--verbose
also lists transitive ones.
Like npm, Bun prefers non-deprecated versions when resolving a range, and
bun add pkg
skips a deprecated
latest
. Versions already in
bun.lock
don't change until you run
bun update
.
minimumReleaseAge supports units and per-version exclusions#
bunfig.toml
[install]
minimumReleaseAge = "3d" # also "90m", "12 hours", "2 weeks"
minimumReleaseAgeExcludes = ["@types/bun", "[email protected]"]
A plain number is still interpreted as seconds.
name@version
excludes a single release, like a security fix you need today, while the rest of that package's versions still have to wait.
Both are also available as flags:
--minimum-release-age 3d
and the new
--minimum-release-age-excludes [email protected]
.
--env-file for the package manager, and new bunx flags#
bun install --env-file ci.env
bunx --env-file ci.env prisma migrate deploy
bunx --cwd packages/api prisma migrate deploy
bun install --env-file ci.env
used to try to install a package named
ci.env
.
--env-file <file>,--no-env-fileandenv = falseinbunfig.tomlwork with every package manager command.bunx --cwd <dir>runs the tool in another directory.bunx --minimum-release-age <seconds>applies the minimum release age to packages thatbunxdownloads.bunx --trustruns lifecycle scripts for the package and all of its dependencies.
bun add --dev moves existing dependencies#
bun add -d
,
--optional
and
--peer
move a package that's already listed in another dependency group. They used to leave it where it was.
bun add -d typescript # "dependencies" -> "devDependencies"
bun add --save-prod typescript # and back
bun install reads the .npmrc that actions/setup-node writes#
actions/setup-node
writes its registry and token settings to a temporary
.npmrc
and points
NPM_CONFIG_USERCONFIG
at it. Bun reads that variable, so private registries work without copying the file.
.github/workflows/ci.yml
- uses: actions/setup-node@v4
with:
registry-url: https://npm.pkg.github.com
- run: bun install
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bun install understands npm, Yarn and pnpm flags, and rejects unknown ones#
bun install
,
bun ci
,
bun add
,
bun remove
and
bun update
translate flags from other package managers when Bun has an equivalent. Bun v1.4.3 ignored them, so
bun add --save-dev zod
saved to
dependencies
and
bun install --immutable
updated the lockfile.
| Flag | Bun equivalent |
|---|---|
--immutable | --frozen-lockfile |
--package-lock-only | --lockfile-only |
--save-dev, --save-exact, --save-optional, --save-peer | --dev, --exact, --optional, --peer |
--save-catalog | --catalog |
--no-optional, --ignore-optional | --omit=optional |
--only=production | --omit=dev |
--location=global | --global |
--shamefully-hoist, --node-linker=hoisted, --install-strategy=hoisted | --linker=hoisted |
--node-linker=isolated, --install-strategy=linked | --linker=isolated |
Flags with no Bun equivalent, like
--no-package-lock
or
--network-timeout 100000
, are ignored along with their value. Bun v1.4.3 only ignored the flag itself, so
bun install --network-timeout 100000
tried to add a package named
100000
.
Flags that none of these tools support are an error.
bun install --frozen-lockfil
error: bun install has no --frozen-lockfil flag.
note: Run 'bun install --help' for more information
Package manager commands and
bunx
used to ignore unknown flags, so this typo would silently run a normal install in CI.
More in bun install#
bun update --depth 0only updates the dependencies in yourpackage.json. Transitive dependencies keep the versions inbun.lock.bun prune --checkprints whatbun prunewould remove without deleting anything, and exits with code 1 if there's anything to remove.publishConfig.registryandpublishConfig["@scope:registry"]set the registry thatbun publishpublishes to.- Like npm 11,
bun publishrefuses to publish a prerelease version like2.0.0-beta.1without--tagorpublishConfig.tag, instead of tagging itlatest. workspace:*can refer to the root package inbun install,bun pm packandbun publish.npm-shrinkwrap.jsonis migrated, likepackage-lock.json.bun.lockrecords which packages have install scripts ("hasInstallScript": true), so installing from a committed lockfile reports blocked scripts. Older versions of Bun ignore the new key and drop it the next time they save the lockfile.- When
bun install --frozen-lockfileorbun cifails becausepackage.jsonchanged, the error names the dependency, such asis-odd was addedoris-number changed from ^6.0.0 to ^7.0.0. --dry-runworks withbun link,bun unlink,bun pm version,bun pm cache rm,bun pm trust,bun pm pkgandbun pm migrate. Before, they accepted the flag but still wrote changes.bun pm pkg setanddeletesupport arrays (keywords[]=cli,contributors[0].name=Jane) and bracketed keys (exports[./package.json]).bun pm viewandbun infowork outside of a project, accept multiple fields at once, and support npm-style paths likemaintainers[0].name.bun info,bun pm view,bun pm whoamiandbun auditretry failed requests likebun installdoes, instead of failing on a single 503 or dropped connection.bun pm lsaccepts package names (bun pm ls lodash),--production,--omitand--depth=0.bun pm versionsets$npm_old_version,$npm_new_version,$npm_commandand$npm_package_versionfor thepreversion,versionandpostversionscripts.- Lifecycle scripts of dependencies get their own
npm_package_name,npm_package_version,npm_package_jsonandnpm_package_config_*, plusnpm_lifecycle_event,npm_lifecycle_scriptandnpm_config_local_prefix. "gypfile": falsein a package disables the implicitnode-gyp rebuild.bun create <template>supports--ignore-scripts. It also skips the template's preinstall and postinstall tasks whenignoreScripts = trueis set inbunfig.tomlorignore-scripts=truein.npmrc.bunx -p pkg cmdinstallspkg, then runscmdwithpkg's binaries first inPATH, like npx.- Auto-install reads registries and credentials from
.npmrc. - Auto-install uses the versions pinned in
bun.lockorbun.lockb. - Auto-install reads the project's
package.jsonthe same waybun installdoes:devDependencies,peerDependencies,overrides,resolutionsandcatalogsare applied, andworkspace:,file:andlink:dependencies are loaded from disk. - Security scanners receive every resolved dependency, including git, GitHub, tarball,
file:andlink:ones. Cache-Control: no-cache,no-storeand amax-ageunder 5 minutes on registry metadata are respected.--use-system-caworks withbun installand the other package manager commands. OnlyNODE_USE_SYSTEM_CA=1worked before.BUN_INSTALL,XDG_CACHE_HOME,BUN_INSTALL_CACHE_DIR,BUN_INSTALL_GLOBAL_DIR,BUN_INSTALL_BINandNPM_CONFIG_USERCONFIGaccept a leading~/.bun audit fixreports and rewrites anoverridesrule that pins a package to a vulnerable version.bun pm migrateon a Yarn v1 lockfile with 20,000 entries takes 0.27 s, down from 24 s.bun installin a monorepo with 2,100 workspaces peaks at 35 MB of memory, down from 91 MB.- Applying a patch no longer takes quadratic time.
bun patch --commitwith 40,000 changed lines in a 100,000-line file takes 0.09 s instead of 1.45 s. - With
globalStore = true, a no-opbun installno longer deletes and re-creates everynode_modules/.bun/<pkg>link. The store rebuilds its entries once after this upgrade, andbun pm cache rmfrees the old ones.
Thanks to @nikelborm for the contribution!
New in Bun.SQL and bun:sqlite#
Bun.SQL uses up to 48% less CPU per query#
We reworked
Bun.SQL
,
bun:sqlite
and
node:sqlite
in a single change that replaces 149 open pull requests and fixes 51 issues.
Every query allocates less memory and executes fewer instructions on the client.
Client-side CPU usage with PostgreSQL, measured in user-space instructions on Linux x64:
| Operation | Change |
|---|---|
Point SELECT | -48% |
100-row SELECT | -30% |
INSERT | -21% |
| Bulk insert, 1,000 rows x 10 columns | -38% |
The same bulk insert uses 53% fewer instructions on MySQL.
Garbage-collected objects allocated per
await sql`select 1`
:
| Adapter | After | Before |
|---|---|---|
| PostgreSQL | 28 | 84 |
| MySQL | 27 | 76 |
| SQLite | 33 | 52 |
query.cancel() cancels a running PostgreSQL query#
cancel()
was already in the types and docs, but it didn't do anything. Now Bun asks the server to cancel the query, and the promise rejects with the server's error.
const query = sql`SELECT pg_sleep(30)`.execute();
setTimeout(() => query.cancel(), 100);
await query; // rejects: canceling statement due to user request
A query that hasn't been sent yet is never sent, and rejects with
ERR_POSTGRES_QUERY_CANCELLED
,
ERR_MYSQL_QUERY_CANCELLED
or
ERR_SQLITE_QUERY_CANCELLED
. On MySQL and MariaDB, a query that's already running on the server still runs to completion.
COPY streams for PostgreSQL#
query.writable()
writes to
COPY ... FROM STDIN
, and
query.readable()
reads from
COPY ... TO STDOUT
. Both are web streams with backpressure, so memory usage stays flat no matter how large the table is.
// Load a CSV file into a table
await Bun.file("users.csv")
.stream()
.pipeTo(sql`COPY users (id, name) FROM STDIN (FORMAT csv)`.writable());
// Stream a table as an HTTP response
new Response(sql`COPY users TO STDOUT (FORMAT csv, HEADER)`.readable());
The streams carry raw bytes in whichever format the statement specifies:
text
,
csv
or
binary
. A 1,000 MiB
COPY
streams at 255 MiB/s with a peak RSS of 59 MiB.
Thanks to @gohryt for the contribution!
Column name transforms in Bun.SQL#
transform: SQL.camel
converts column names in both directions:
first_name
is read as
firstName
, and
firstName
is written as
first_name
. It works with every adapter.
import { SQL } from "bun";
const sql = new SQL({ transform: SQL.camel });
await sql`INSERT INTO users ${sql({ firstName: "Alice" })}`;
// INSERT INTO users ("first_name") VALUES ($1)
const [user] = await sql`SELECT first_name FROM users`;
user.firstName; // "Alice"
SQL.camel,SQL.pascalandSQL.kebabconvert in both directions.SQL.toCamelonly converts results, andSQL.fromCamelonly converts names passed tosql()helpers.pascalandkebabhave the same variants.{ column: { from, to } }accepts custom functions.
The names match postgres.js.
Thanks to @pjb3 for the contribution!
Pass JavaScript arrays to PostgreSQL array columns#
You can pass a JavaScript array or an
Int32Array
anywhere PostgreSQL expects a built-in array type, including arrays with
null
s and nested arrays. Both of these queries used to fail.
await sql`SELECT * FROM users WHERE id = ANY(${[1, 2, 3]})`;
// tags text[]
await sql`INSERT INTO posts ${sql({ title: "Hello", tags: ["bun", "sql"] })}`;
35 more built-in array types, such as
uuid[]
,
int4range[]
and
tsvector[]
, are returned as arrays instead of the string
"{...}"
.
For an array of a user-defined type such as an enum, use
sql.array(values, "mood")
.
result.columns and result.statement#
On PostgreSQL, MySQL and MariaDB, results include the column metadata reported by the server. Use
type
to distinguish columns that produce the same JavaScript value, such as
jsonb
and
text[]
.
const result = await sql`SELECT id, tags FROM posts WHERE id = ${1}`;
result.columns;
// [
// { name: "id", type: 23, table: 16388, number: 1 }, // int4
// { name: "tags", type: 1009, table: 16388, number: 2 }, // text[]
// ]
result.statement.string; // the text Bun sent, with $1 for the parameter
result.statement.types; // [23]
MySQL and MariaDB columns are
{ name, type, table, length, flags }
. Both properties are
null
on SQLite.
sql.json() #
sql.json(value)
stores any value as JSON and works with every adapter. Use it for numbers, booleans, strings or
null
.
await sql`INSERT INTO settings (data) VALUES (${sql.json(42)})`; // 42
await sql`INSERT INTO settings (data) VALUES (${sql.json("dark")})`; // "dark"
await sql`INSERT INTO settings (data) VALUES (${sql.json(null)})`; // JSON null
A bare
null
is still SQL
NULL
.
bun --hot reuses database connections#
Every save under
bun --hot
used to open up to
max
new PostgreSQL or MySQL connections and leave the old ones open. Now a
new SQL()
with the same settings reuses the connections from before the reload.
With
max: 2
, five reloads used to leave 12 sessions open on the server. Now there are 2.
Session state like
SET
and temporary tables is preserved.
onconnect
runs again for each connection that's reused.
Thanks to @eriklangille for the contribution!
New Bun.SQL connection options#
const sql = new SQL("postgres://[email protected]/app?connect_timeout=5", {
channelBinding: "require",
tls: {
ca: Bun.file("./ca.pem"),
checkServerIdentity: (hostname, cert) =>
cert.fingerprint256 === pin ? undefined : new Error("pin mismatch"),
},
});
channelBinding:"disable"(the default),"prefer"or"require". It binds PostgreSQL SCRAM authentication to the server's TLS certificate (SCRAM-SHA-256-PLUS). Also available as?channel_binding=andPGCHANNELBINDING.tls.checkServerIdentity(hostname, cert): replaces the built-in hostname check on PostgreSQL, MySQL and MariaDB, like innode:tls. Passing it upgrades any SSL mode belowverify-catoverify-full.foundRows: true: on MySQL and MariaDB,affectedRowscounts the rows anUPDATEmatched instead of the rows it changed, likemysql2. Also available as?foundRows=true.?connect_timeout=,?idle_timeout=,?max_lifetime=and?max=in PostgreSQL and MySQL URLs.- libpq's URL parameters on PostgreSQL:
host,port,user,password,dbname,sslrootcert,sslcert,sslkeyandsslpassword. Bun used to forward them to the server, which refused the connection. PGAPPNAME,PGOPTIONSandPGCONNECT_TIMEOUTenvironment variables.
User-defined functions in bun:sqlite#
db.function()
defines a SQL function that calls a JavaScript function.
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
db.function("add2", (a, b) => a + b);
db.query("SELECT add2(12, 4) AS sum").get(); // { sum: 16 }
db.aggregate()
defines an aggregate function such as
sum()
.
db.aggregate("add_all", {
start: 0,
step: (total, value) => total + value,
});
db.query("SELECT add_all(dollars) AS total FROM expenses").get();
- Both accept the
varargs,deterministic,directOnlyandsafeIntegersoptions. inverselets an aggregate be used as a window function.- Errors thrown by a function propagate to the
get(),all()orrun()call that ran the query.
The signatures match better-sqlite3.
Thanks to @nikeee and @jdjkelly for the contribution!
Update hooks in bun:sqlite#
Set
db.onInsert
,
db.onUpdate
or
db.onDelete
to a function, and SQLite calls it for each row that this
Database
inserts, updates or deletes.
db.onInsert = (db, tableName, rowid) => {
console.log(`inserted ${tableName} ${rowid}`);
};
db.run("INSERT INTO cats (name) VALUES ('Salem'), ('Keanu')");
// inserted cats 1
// inserted cats 2
Set it to
null
to remove the hook.
Also new:
stmt.readeristrueif the statement returns rows.stmt.readonlyistrueif the statement doesn't directly modify the database.db[Symbol.for("sqlite-type")]is"bun:sqlite". Libraries can use it to distinguish aDatabasefrom anode:sqliteDatabaseSync.
Thanks to @jecquas and @himself65 for the contribution!
New in WebSocket and ws#
ws behaves like the npm package on Node.js#
import "ws"
in Bun loads a built-in implementation backed by Bun's native WebSocket client and server. We rewrote it as a port of
ws
8.22.0. Framing, masking, UTF-8 validation and compression are still native.
Compared to npm ws on Node.js | Bun v1.4.4 | Before |
|---|---|---|
ws's own test suite, 271 tests | 234 | 94 |
| 212 client scripts, byte-for-byte identical output | 179 | 18 |
| 159 server scripts, byte-for-byte identical output | 150 | 67 |
The remaining failures depend on
ws
internals that the native implementation doesn't have, like
ws._receiver
and
generateMask
.
import { WebSocket, createWebSocketStream } from "ws";
const ws = new WebSocket("wss://example.com/logs", {
handshakeTimeout: 5_000,
followRedirects: true,
maxPayload: 1024 * 1024,
});
createWebSocketStream(ws, { encoding: "utf8" }).pipe(process.stdout);
createWebSocketStream()is implemented, with backpressure. It used to throw "Not supported yet in Bun".agent,createConnectionandlookupare respected, so SOCKS proxy agents work. A customagentused to be ignored, and the client connected directly.handshakeTimeout,closeTimeout,followRedirects,maxRedirects,origin,auth,protocolVersion,autoPongandskipUTF8Validationare implemented instead of silently ignored.maxPayloadis enforced on clients and servers. socket.io'smaxHttpBufferSizedepends on it.WebSocketServer({ perMessageDeflate })negotiates compression.WebSocketServeraccepts anyDuplex. This includeshttp2.createSecureServer({ allowHTTP1: true })(vite --https),server.emit("connection", socket)(socket.io sticky sessions) and fastify'sinjectWS.- Server sockets have
pause(),resume()andisPaused. They'reinstanceof WebSocket, andws._socketis thenode:httpsocket.
ws events match Node.js#
ws.on("error", (error) => {
error instanceof Error; // true, was an ErrorEvent
error.code; // "ECONNREFUSED"
});
ws.on("close", (code, reason) => {
reason; // a Buffer, was a string
});
- An
'error'event without a listener is an uncaught exception, like in Node.js. Add an'error'listener to every client and every server socket. 'close'fires when the connection closes, with the peer's code and reason.close()without a code reports 1005, and a failed connection reports 1006.- Error messages match the npm package's, like
Unexpected server response: 403. send()throws while the socket isCONNECTING.wsservers have no idle timeout, no automatic pings and no backpressure limit. They used to inheritBun.serve's defaults: a 120 second idle timeout, a ping at 104 seconds and a 16 MiB limit.- The default
maxPayloadis 100 MiB, up from 16 MiB.
WebSocket waits for the closing handshake#
The
close
event fires once the connection has closed, like in browsers and Node.js. It used to fire right after
close()
with your own code and
wasClean: true
, regardless of what the server did.
const ws = new WebSocket("wss://example.com");
ws.onopen = () => ws.close();
ws.onclose = (event) => {
event.code; // the server's code, 1005 if its Close frame had none
event.wasClean; // false if the server never sent a Close frame
};
close()without a code sends an empty Close frame. Both ends report 1005 instead of 1000, including theclose(ws, code)handler inBun.serve.- A script that ends with
ws.close()takes one extra round trip to exit. If the server doesn't respond within 30 seconds,closefires with 1006. BUN_CONFIG_WS_CLOSE_TIMEOUTchanges the 30 second timeout.ws.terminate()closes immediately.
Failed WebSocket connections fire error, then close with 1006#
A bad handshake, a TLS or proxy error, a protocol error, an oversized message and invalid UTF-8 are all reported the same way, like in browsers and Node.js.
event.error.code
is new.
const ws = new WebSocket("ws://localhost:1");
ws.onerror = (event) => {
event.error.code; // "ECONNREFUSED"
};
ws.onclose = (event) => {
event.code; // 1006
event.reason; // ""
event.wasClean; // false
};
Before,
close
fired with 1002, 1007, 1009, 1011 or 1015 and an English reason, sometimes with
wasClean: true
, and often no
error
event fired.
event.messagehas the description that used to be inreason, with more detail:Expected 101 status code: 503,TLS handshake failed: self signed certificate.event.error.codeis a Node.js-style code such asENOTFOUND,DEPTH_ZERO_SELF_SIGNED_CERT,ERR_TLS_CERT_ALTNAME_INVALIDorWS_ERR_INVALID_UTF8.- Losing the TCP connection after
openisn't treated as a failure. Onlyclosefires, with 1006.
ws.pause() and ws.resume() in Bun.serve#
Use these to slow down a client that sends faster than you can process. While a socket is paused, Bun stops reading from it, so the client sees TCP backpressure.
Bun.serve({
fetch(req, server) {}, // upgrade logic
websocket: {
message(ws, message) {
if (!file.write(message)) {
ws.pause();
file.once("drain", () => ws.resume());
}
},
},
});
ws.isPaused
tells you whether the socket is paused. The idle timeout is suspended while a socket is paused.
websocket.error in Bun.serve receives the socket#
When a
websocket
handler throws, Bun calls
error
with the error and the socket it came from. It only received the error before, and it was missing from the types.
Bun.serve({
fetch(req, server) {}, // upgrade logic
websocket: {
message(ws, message) {
throw new Error("oops");
},
error(error, ws) {
ws.close(1011);
},
},
});
Every handler is optional, including
message
.
proxy.tls: separate TLS options for an HTTPS proxy#
With an
https://
proxy,
tls
applies to both the proxy and the server. Pass
proxy.tls
to configure them separately. The proxy then only uses
proxy.tls
, and the server only uses
tls
.
const ws = new WebSocket("wss://example.com", {
proxy: {
url: "https://proxy.internal:8443",
tls: { ca: Bun.file("./proxy-ca.pem") },
},
});
Thanks to @steipete, @hughescr, @davidhevn and @deepshekhardas for the contributions!
New in the runtime#
JavaScriptCore upgrade#
This release upgrades JavaScriptCore with 1,013 upstream WebKit commits.
Measured on an Apple silicon Mac, best of several runs:
| Operation | Bun v1.4.4 | Bun v1.4.3 | Faster |
|---|---|---|---|
n.toLocaleString("en-US"), 100,000 calls | 25 ms | 1,341 ms | 54x |
| Loading an array literal of 1,000,000 integers | 11 ms | 45 ms | 4.2x |
for (const ch of str), 1,000,000 characters, some non-Latin-1 | 1.2 ms | 3.2 ms | 2.7x |
new Array(n) with n under 4, 10,000,000 calls | 31 ms | 61 ms | 2.0x |
try { throw "x" } catch {}, 1,000,000 times | 37 ms | 72 ms | 2.0x |
try { throw new Error("x") } catch {}, 1,000,000 times | 128 ms | 174 ms | 1.4x |
new WeakMap() and one set(), 1,000,000 times | 7.7 ms | 9.9 ms | 1.3x |
toLocaleString()
with a locale and no options reuses a cached
Intl.NumberFormat
, which makes it as fast as creating the formatter yourself and calling
format()
. This applies to numbers and BigInts.
usingis allowed in a function nested inside aswitchcase. It was aSyntaxError.+1n * 2nthrows aTypeError. It used to return2n.+a * b()callsa.valueOf()beforeb().for...inover a function visits its indexed properties, likefn[0].- JIT-optimized
(x + y + 0.5) | 0returns the right result whenx + yoverflows 32 bits. - JIT-optimized code throws a
RangeErrorfornew Uint8Array(-1)even if the array is never used. - In sloppy mode,
label: function f() {}is hoisted like an unlabeled function declaration. - WebAssembly's baseline compiler keeps a function's most-used locals in registers.
Bun.Glob supports extglob and brace ranges#
const source = new Bun.Glob("**/!(*.test).ts");
source.match("src/cart.ts"); // true
source.match("src/cart.test.ts"); // false
new Bun.Glob("src/@(app|lib)/**/*.ts").match("src/app/a/b.ts"); // true
new Bun.Glob("shard-{1..3}.json").match("shard-2.json"); // true
| Pattern | Matches |
|---|---|
| `?(a | b)` |
| `*(a | b)` |
| `+(a | b)` |
| `@(a | b)` |
| `!(a | b)` |
{1..3}, {01..10}, {a..e}, {1..9..2} | A number or letter in the range. The third value is the step. |
The new syntax works everywhere Bun uses globs, including
--filter
and
workspaces
,
files
and
sideEffects
in
package.json
.
package.json
{
"workspaces": ["packages/!(internal)"]
}
In Bun v1.4.3, this failed with
error: Workspace not found "packages/!(internal)"
.
A
(
that isn't preceded by
?
,
*
,
+
,
@
or
!
is still a literal character, so Next.js route groups like
app/(marketing)/**
still match.
These patterns used to be matched literally. They now have a different meaning, consistent with picomatch, minimatch and bash's
extglob
:
| Pattern | Bun v1.4.3 | Bun v1.4.4 |
|---|---|---|
*(1).txt | Matched copy(1).txt | Zero or more 1, then .txt. Write *[(]1).txt instead. |
!(g)/a.ts | Everything but (g)/a.ts | a.ts in any folder but g |
{1..3} | The text 1..3 | 1, 2 or 3 |
This only affects patterns, not paths. A file with those characters in its name still matches
**/*.ts
.
Bun Shell is unaffected: quoted text and
${}
values are still treated literally.
Up to 2.8x faster Headers iteration#
for...of
,
entries()
,
keys()
,
values()
,
forEach()
and
Object.fromEntries(headers)
are 1.7x to 2.8x faster. Adapters that copy a
Response
's headers into
node:http
, like
@hono/node-server
, do this on every request.
for (const [name, value] of response.headers) {
res.setHeader(name, value);
}
Time to iterate over one
Headers
object on an M4 Max, best of 7 runs:
| Operation | v1.4.4 | v1.4.3 | Faster |
|---|---|---|---|
for...of, 3 headers | 98 ns | 277 ns | 2.8x |
for...of, 14 headers | 627 ns | 1.41 µs | 2.2x |
for...of, 14 custom X- headers | 1.24 µs | 2.08 µs | 1.7x |
headers.forEach(), 14 headers | 505 ns | 1.10 µs | 2.2x |
[...headers.keys()], 14 headers | 261 ns | 688 ns | 2.6x |
[...headers.values()], 14 headers | 295 ns | 680 ns | 2.3x |
Object.fromEntries(headers), 14 headers | 579 ns | 1.46 µs | 2.5x |
Up to 2.6x faster response.body streams for in-memory bodies#
Streaming a body that's already in memory (
new Response("...").body
,
request.body
,
blob.stream()
) is 1.5x to 2.6x faster. Reading a 100-byte body allocates 7 KB instead of 270 KB.
for await (const chunk of new Response(html).body) {
res.write(chunk);
}
Time to read an entire
new Response(string).body
with
reader.read()
, on Linux x64:
| Body | After | Before |
|---|---|---|
| 100 B | 1.41 µs | 3.66 µs |
| 4 KB | 1.77 µs | 4.15 µs |
| 16 KB | 2.93 µs | 5.54 µs |
| 64 KB | 9.11 µs | 14.33 µs |
| 1 MB | 176 µs | 272 µs |
Adapters that copy a
Response
into a
node:http
response read the body this way. A Hono app on
@hono/node-server
handled a batch of 24 requests in 16.0 ms, down from 24.0 ms.
--disallow-code-generation-from-strings=strict only runs code that ships with your program#
=strict
used to block a fixed list of APIs. Now JavaScriptCore refuses to run any code that isn't a Bun built-in or one of your program's own files, regardless of which API it came from.
For a
bun build --compile
executable, only the code inside the executable counts as your program. JavaScript and TypeScript files on disk no longer run, including files the program just wrote:
const path = "/tmp/plugin.js";
await Bun.write(path, "console.log('hi')");
await import(path);
// EvalError: Code generation from strings disallowed for this context
Without
--compile
, files on disk count as your program, so that import still runs.
=strict
also blocks:
- Plugins that return
contents, for any loader. Return anexportsobject instead. - Macros.
- In a compiled executable,
--preloadinBUN_OPTIONSand.txtor.mdimports from disk.
.json
,
.toml
and
.yaml
files still load from disk.
import()
of a
data:
URL containing JSON is no longer blocked.
New in the dev server#
Redesigned error overlay in the dev server#
The error overlay for
Bun.serve
with HTML imports has a new design, and similar errors in the same file are grouped into one card. Three missing fonts in a stylesheet used to show up as three separate blocks.
BUILD FAILED
4 errors in 2 files
app.ts 1 ERROR
ERROR Could not resolve: "./render"
1 import { render } from "./render";
styles.css 3 ERRORS
ERROR Could not resolve ×3
3 src: url("./fonts/inter-regular.woff2");
8 src: url("./fonts/inter-medium.woff2");
13 src: url("./fonts/inter-bold.woff2");
- The overlay scrolls. Errors past the bottom of the window used to be unreachable.
- Build errors are shown once, not twice, when a file is bundled for both the server and the browser, like a stylesheet imported by a page.
- There's a light theme that follows your system setting.
- Runtime errors use the same design, with the error's name as the title.
Other improvements#
-
Improved:
req.headersandres.setHeader()have less overhead innode:httpservers. 256 requests through Express 5 take 9.45 ms, down from 9.91 ms (Linux x64). -
Improved:
Bun.JSON5.parse()is up to 1.6x faster. A 126 KBbun.lockparses in 0.42 ms, down from 0.67 ms (M4 Max). -
Improved:
Bun.Glob.match()is 1.6x to 5x faster. Matching**/*.{ts,tsx}againstsrc/components/button/index.tsxtakes 32 ns, down from 158 ns (M4 Max). -
Improved: root certificates are updated to NSS 3.131.1.
-
Improved: native addons on Linux and macOS can call 47 more libuv functions, including
uv_async_*,uv_timer_*,uv_idle_*,uv_prepare_*,uv_check_*,uv_queue_work,uv_default_loopanduv_cwd. Bun used to exit with "unsupported uv function". -
Improved: echoing a 1 MB non-ASCII WebSocket text message in
Bun.servetakes 72% fewer CPU instructions. -
Improved:
Bun.markdown.html()on large non-ASCII input takes 33% fewer CPU instructions. -
Improved:
Bun.hash()and its algorithms are 1.2x to 2.3x faster on short strings.Bun.hash.crc32()on a 16-character ASCII string takes 8 ns, down from 15 ns (M4 Max). -
Improved:
Bun.CryptoHasher,Bun.SHA256and the other hashers are 1.2x to 1.7x faster on small inputs. Hashing 16 bytes to hex withBun.SHA256.hash()takes 77 ns, down from 107 ns (M4 Max). -
Improved:
new TextDecoder()with an options object, likenew TextDecoder("utf-8", { fatal: true }), is 2x faster. -
Fixed: a regression in Bun v1.4.3 where runs that write the compile cache (
NODE_COMPILE_CACHEormodule.enableCompileCache()) took 2x longer to exit (580 ms instead of 300 ms for a 3.4 MB module). -
Improved: Bun's FreeBSD binaries are built against FreeBSD 14.4. The previous target, 14.3, has reached end of life.
Security#
This release includes security fixes for TLS, WebSocket, the database clients and the package manager. These protocols, APIs and commands are affected:
TLS, HTTP/1.1, HTTP/2, HTTP/3, WebSocket, Brotli,
Bun.serve
,
Bun.listen
,
Bun.connect
,
Bun.SQL
,
Bun.RedisClient
,
Bun.password
,
Bun.Image
,
Bun.Archive
,
node:tls
,
node:https
,
node:http
,
node:http2
,
node:net
,
node:quic
,
node:crypto
,
node:vm
,
node:v8
,
ws
,
fetch
,
TextEncoder
,
bun install
,
bunx
,
bun patch
,
bun publish
,
bun pm pack
,
bun pm trust
,
bun audit
and
bun create
.
Bugfixes#
Node.js compatibility improvements#
- Fixed: various stability issues in
node:http,node:http2,node:quic,node:dgram,node:dns,node:crypto,node:vm,node:stream,node:zlib,node:util,node:diagnostics_channel,node:worker_threadsand native addons.
node:http
- Fixed: in a
node:httpserver,res.end()threwERR_INVALID_ARG_TYPEwhenres.statusCodewas a numeric string like"404"or a fraction like200.5. It now sends404and200, like Node.js. - Fixed: in a
node:httpserver,res.end()sent200whenres.statusCodewasNaNorundefined. It now throwsERR_HTTP_INVALID_STATUS_CODE, like Node.js. - Fixed: in a
node:httpserver, afterres.end({})threw because of the invalid chunk,res.headersSentwastrueeven though nothing had been sent, sores.setHeader()threwERR_HTTP_HEADERS_SENT.
node:http2
- Fixed:
pushStream()innode:http2threwERR_HTTP2_OUT_OF_STREAMSinstead of passing it to the callback. - Fixed: with
http2.createSecureServer()andserver.emit("connection", socket),session.socketwasn't aTLSSocketand'connection'listeners didn't fire.
node:fs
- Fixed: on Linux and macOS, every
fs.realpath()variant treated\as a path separator, so a file nameda\bthrewENOENT, or resolved toa/bif that existed. - Fixed: on Linux and macOS,
fs.realpath.native(),fs.realpathSync.native()andfs.promises.realpath()collapsed..before following symlinks, solink/../yresolved next to the link instead of its target, andmissing/..didn't throw. - Fixed: on Linux and macOS,
fs.realpath()andfs.realpathSync()threwENOTDIRfor a regular file's path with a trailing slash, likefile/. Like Node.js, they now return the file's path. - Fixed: on Windows,
fs.promises.realpath("C:\\")returnedC:instead ofC:\.
node:vm
- Fixed:
vm.createContext(vm.constants.DONT_CONTEXTIFY)returned a wrapper instead of the context's global. Top-levelvarandfunctiondeclarations were lost, and Vitest with jsdom 30.1 failed to start. - Fixed: in a
vm.createContext(vm.constants.DONT_CONTEXTIFY)context, scripts threw aReferenceErrorfor names inherited from the global's prototype chain, including through aProxy. jsdom 30 withrunScriptsrelies on this.
node:stream
- Fixed:
Readable.fromWeb(stream).destroy()emitted'error'when the web stream'scancel()rejected. Node.js doesn't.
node:crypto
- Fixed: an X25519
CryptoKeycouldn't be cloned withstructuredClone()or sent withpostMessage(). - Fixed:
write()on anode:cryptoHashorHmacthrewERR_CRYPTO_HASH_FINALIZEDafterdigest(). The chunk is now dropped, like in Node.js.
node:util
- Fixed: calling
newon a native function that isn't a constructor, likeutil.types.isDate, returned the result of calling it. It now throws aTypeError, like Node.js. These functions were also printed as[class isDate]. - Fixed:
util.types.isProxy(globalThis)returnedtrue.
process
- Fixed: registering the same function twice with
process.on()only added it once. It's now added and called twice, like in Node.js. - Fixed: after
import * as process from "node:process",process.on()didn't add the listener toprocess.
node:diagnostics_channel
- Fixed: in
node:diagnostics_channel, the next subscriber was skipped when a subscriber unsubscribed itself duringpublish(). - Fixed: in
node:diagnostics_channel, a subscriber added duringpublish()was called during that samepublish(). - Fixed:
channel.unsubscribe()threw aTypeErrorwhenchannel._subscribersheld a frozen or sealed array.
node:worker_threads
- Fixed: in a
Workerwithenv: SHARE_ENV,join(),slice()andconcat()treated holes as empty in an array withprocess.envin its prototype chain.arr[1]already readprocess.env[1].
Native addons
- Fixed: loading a
NODE_MODULEaddon in a Worker when the main thread has already loaded it now throwsERR_DLOPEN_FAILED, like Node.js. - Fixed: when a Worker stopped while a native addon's async work was still running, the addon's finalizers ran before that work was done. They now run after it, like in Node.js.
Other modules
-
Fixed: the
write()callback on anode:netornode:tlssocket was never called if the connection closed while the write was still pending. It's now called withECANCELED, like in Node.js. -
Fixed:
os.userInfo()threwTypeError: Type errorwhen passed a number or a string. It now behaves like Node.js. -
Fixed:
os.setPriority(priority, undefined)threw. It now behaves like Node.js. -
Fixed:
socket.connect()innode:dgramfailed withERR_INVALID_ARG_TYPEwhen the port was a numeric string like"41234". Node.js accepts it. -
Fixed:
zlib.crc32(data, undefined)threwERR_INVALID_ARG_TYPE. It now starts from0, likezlib.crc32(data). -
Fixed:
http.maxHeaderSize = "5000000000"wrapped to705032704. The setter now only accepts a safe integer of 1 or more, and a string like"16384"throwsERR_INVALID_ARG_TYPE. -
Fixed:
new SourceMap()fromnode:modulethrewERR_INVALID_ARG_TYPEwhenmappingswas an empty string, like in the source map of a chunk that only re-exports. -
Fixed:
Profiler.takePreciseCoverageinnode:inspectorreturned function and block ranges that didn't match V8's, so tools likev8-to-istanbulmarked the wrong lines. A skipped?? fallback()was counted as executed.
Bun APIs#
- Fixed: various stability issues in
Bun.serve,Bun.spawn,FileSink,Bun.S3Client,Bun.RedisClient,Bun.YAML,Bun.JSON5,Bun.markdown,Bun.Cookie,Bun.WebView,HTMLRewriter,bun:ffi,Bun.gzipSync(),Bun.deflateSync()andBun.unsafe.arrayBufferToString().
Bun.serve
- Fixed: an error thrown by a
Bun.serverequest handler in a Worker is now printed, like on the main thread.
Bun.hash and Bun.CryptoHasher
- Fixed:
Bun.hash()and its algorithms returned the hash of an empty input when given aBun.file(), an S3 file or aBun.build()output written tooutdir. They now throw aTypeError. - Fixed: hashing an S3 file with
Bun.CryptoHasher,Bun.SHA1.hash(),Bun.sha()orcrypto.hash()returned the digest of an empty input. It now throws aTypeError, like it does forBun.file(). - Fixed:
Bun.hash(),Bun.hash(undefined)andBun.hash(null)returned the hash of"","undefined"and"null". They now throw aTypeError, and so doBun.hash.crc32()and the other algorithms. - Fixed: accessing the static
byteLengthon a subclass ofBun.MD5,Bun.SHA256or another hasher threw aTypeError, and so didalgorithmson a subclass ofBun.CryptoHasher.
Bun.Glob
-
Fixed:
Bun.Glob.match()treated**inside braces like*, so{src/**,lib/**}didn't matchsrc/a/b.ts. -
Fixed:
Bun.Glob.match()returnedfalsefor a pattern with more than 10 brace groups, or with braces nested more than 10 deep. -
Fixed:
Bun.Glob'sscan()andscanSync()treated a truthy option that isn'ttrueasfalse.{ dot: 1 }skipped dotfiles and{ onlyFiles: 1 }listed directories.
Other Bun APIs
-
Fixed:
Bun.JSON5.parse()accepted two invalid inputs."1\0 2"parsed as1and"{a\\u002d: 1}"as{ a: 1 }. Both throw aSyntaxError. -
Fixed:
Bun.markdown.ansi()andbun file.mdcould break a line in the middle of a word made of several parts, like`code`followed by.or(followed by a link. -
Fixed:
.ptron abun:fficc()symbol wasn't the address of the C function. -
Fixed:
Bun.YAML.parse()returnednullandBun.TOML.parse()returned{}when given an S3 file. They now throw aTypeError, like they do forBun.file(). -
Fixed:
Bun.gzipSync()andBun.deflateSync()ignored thememLevelandstrategyoptions and always compressed with the defaults. AmemLeveloutside 1 to 9 or astrategyoutside 0 to 4 now throws. -
Fixed:
Bun.CSRF.generate(undefined, options)threw "Secret is required", so there was no way to pass options and keep the default secret. -
Fixed:
Bun.$.escape()returned non-string arguments likenull,trueornew String("foo")unchanged when they didn't need escaping. It now always returns a string. -
Improved:
expiresIn: NaNinBun.CSRF.generate(),maxAge: NaNinBun.CSRF.verify()and aNaNport inBun.dns.prefetch()throw aRangeErrorinstead of falling back to the default. -
Fixed:
Bun.indexOfLine()returned-1when its first argument wasn't a buffer, such as a string that contains a newline. It now throws aTypeError. -
Fixed:
Bun.JSONL.parseChunk()reported areadoffset before a value it had already returned when that value was followed by a stray,,]or}. Regression in v1.4.3. -
Fixed:
Bun.JSONL.parse()andBun.JSONL.parseChunk()returned a value more than once when it was preceded by blank lines and followed by a stray,,]or}. Regression in v1.4.3. -
Fixed:
assetPrefixon aBun.FileSystemRouterwas alwaysundefined. It now returns theassetPrefixoption, ornullif it isn't set. -
Fixed:
queryandparamsfromBun.FileSystemRouter'smatch()listed numeric keys twice, soObject.values(router.match("/?0=a").query)returned["a", "a"]. -
Fixed:
graph.dispose()on aBun.ModuleGraphleaked the graph's modules when its code still had aBun.Terminalopen or anode:httpserver listening. The terminal's file descriptors leaked too.
Web APIs#
- Fixed: various stability issues in experimental HTTP/2
fetch(),ReadableStream.from(),crypto.subtle,structuredClone,URL,URLSearchParams,URLPattern,HeadersandFormData.
fetch
- Fixed:
fetch()withkeepalive: false, or akeepAlive: falseBun.FetchSession, left its HTTP/1.1 connection open in the pool when the request also had aConnection: keep-aliveheader, which happens when you forwardreq.headers.
Streams and Blob
- Fixed:
new Response(blob, { headers }).blob()settypeon the originalBlobwhen it had none. - Fixed:
slice()without a type on a bodyBlobkept the parent's type instead of"".
Other Web APIs
- Fixed:
Reflect.construct()ofWebSocket,BroadcastChannelorWorkerstill connected, subscribed or started the thread when thenewTarget'sprototypegetter threw.
TLS#
-
Fixed: various stability issues in TLS clients and servers.
-
Improved:
Bun.serve,Bun.listenandnode:tlsservers now only request a client certificate whenrequestCert: trueis set, which matches the docs and Node.js. Settingcaalone no longer does. For mTLS, set both. -
Fixed: a
tls.createServer()withSNICallbacknever negotiated ALPN.ALPNProtocolsandALPNCallbackwere ignored, so HTTP/2 clients fell back to HTTP/1.1. -
Fixed: the
wspackage ignored top-level TLS options such asrejectUnauthorized,ca,certandkey, so connecting to a self-signedwss://server failed withTLS handshake failed.pfxis also supported. -
Fixed:
node-fetchignored the TLS options (ca,cert,key,rejectUnauthorized) of anhttps.Agentpassed asagent. -
Fixed:
fetch(),tls.connect()and other TLS clients failed to connect to servers with an Ed25519 certificate. -
Fixed: a self-signed certificate that isn't marked as a CA, like the ones
dotnet dev-certscreates, wasn't trusted when passed inNODE_EXTRA_CA_CERTSorca. -
Fixed: a TLS server configured with multiple
keyandcertpairs only served the last one. With an RSA and an ECDSA pair, each client now gets one it supports. -
Fixed: assigning
tls.DEFAULT_CIPHERShad no effect onfetch,WebSocket,Bun.connect,RedisClient,Bun.SQLandS3Client. -
Fixed:
Bun.SQLwithsslmode=prefertimed out against a Postgres server without TLS. It now falls back to an unencrypted connection, as documented. -
Fixed:
Bun.SQLignoredtls.servername, thenode:tlsspelling ofserverName. -
Fixed:
tls.connect()withautoSelectFamilyadded an'end'listener for each address it tried, and printedMaxListenersExceededWarningafter nine. -
Fixed: the
tlsoptionskey,cert,ca,crlandALPNProtocolsread the entire underlyingArrayBufferwhen given a view into part of it, likebuffer.subarray(n).
WebSocket#
- Fixed: various stability issues in the
WebSocketclient andws.
WebSocket client
-
Fixed: a client
WebSocket'ssend(),ping()andpong()sent the text[object BuildArtifact]when given an in-memoryBun.build()output. They now send its bytes, likeServerWebSocket. -
Fixed:
ws.send()right beforews.close()could lose messages onws://if the server was still sending, whileclosereported a clean 1000. -
Fixed: on
wss://through a proxy, Close and Pong frames could be sent ahead of queued messages, which were then lost even though the close was reported as clean. -
Fixed: a
wss://WebSocketconnected through a proxy closed with 1006 when a frame was sent while a TLS 1.2 server was renegotiating. Direct connections weren't affected. -
Fixed: in rare cases under backpressure, the end of the send queue was never flushed and
bufferedAmountstayed above 0. -
Fixed: event listeners didn't run in the
AsyncLocalStoragecontext of thenew WebSocket()call. -
Fixed:
new WebSocket(url, { protocols: "chat" })threw "Value is not a sequence", andprotocol: "chat"was ignored. -
Fixed:
ws://:token@hostsent no credentials. -
Fixed: one invalid entry in
proxy.headerssilently dropped all of them, includingProxy-Authorization. It now throws instead. -
Fixed: a proxy's 407 was reported as "Proxy connection failed", and a CONNECT reply of
HTTP/1.1 200with no reason phrase, or any 2xx other than 200, was rejected. -
Fixed:
100 Continueor103 Early Hintsbefore the 101 failed the handshake. -
Fixed:
send(),ping()andpong()withBun.file()or an S3 file sent an empty frame. They now throw. -
Fixed: non-ASCII header values were sent as UTF-8. They're now sent as Latin-1, like
fetchand Node.js. -
Fixed: several
WebSocketTypeScript types.errorandonerrorreceive anErrorEvent, thepingandpongevents andbinaryType: "blob"are declared, andheadersaccepts aHeadersobject.
Bun.serve WebSocket servers
- Fixed: Safari closed the connection with 1002 when a server with
perMessageDeflate: truesent compressed messages. - Fixed:
perMessageDeflate.compressset to"dedicated"or"4KB"through"256KB"was treated as"3KB". Each value now uses its own window size, which uses more memory:"dedicated"goes from 149 KB to 282 KB per connection. - Fixed:
perMessageDeflate: { compress: false, decompress: true }still compressed outgoing messages, anddecompress: "16KB"configured the compressor. - Fixed:
server.stop(true)closed WebSockets with 1006. They now close with 1001 "Server closed". - Fixed: after a protocol error, invalid UTF-8 or a message over
maxPayloadLength, the client saw 1006. It now receives a Close frame with 1002, 1007 or 1009 and a reason. - Fixed:
idleTimeout: 0closed idle connections after about 256 seconds instead of never. - Fixed:
idleTimeout: 65536,maxPayloadLength: 2 ** 32 + 16andbackpressureLimit: 2 ** 32 + 1wrapped around to 0, 16 and 1. - Fixed:
maxPayloadLengthalso applied to Ping, Pong and Close frames. - Fixed:
ws.close()on a socket overbackpressureLimitdropped messages thatsend()had already accepted, along with the Close frame. - Fixed:
ws.close()insidews.cork(),drain, oropenafter an async upgrade closed the connection 16 seconds late. - Fixed:
terminate(), a throwingopenhandler,stop(true)and process exit dropped data thatsend()orpublish()had accepted in the same tick. - Fixed: a long
ws.close(code, reason)reason was truncated in the middle of a character, so the client saw 1007. - Fixed:
server.upgrade(req, { headers })sentSec-WebSocket-Accept,UpgradeorConnectiontwice whenheadershad them, threw forheaders: {}, and couldn't decline a subprotocol. An emptySec-WebSocket-Protocolnow declines it. - Fixed: an error thrown after
server.upgrade()was silently dropped. It's now passed toerror()or printed to stderr. - Fixed:
server.reload()leaked memory for every route of a server with WebSockets, and newwebsocketoptions didn't apply to sockets that were already open.
ws
- Fixed: a
wsserver sending a burst of large messages delivered some twice and some out of order. - Fixed:
ws.once("message")together withws.on("message")delivered each message twice. - Fixed: on server sockets,
addEventListener("message")received aBufferfor text, andonclosereceived a number. - Fixed: the
binaryoption ofsend()was ignored for anArrayBufferorBlob, and{ fin: false }sent whole messages instead of fragments. - Fixed: the
send()callback reported success for a message dropped afterclose(). Callbacks now run once the data is written, or with an error if the socket closes first. - Fixed: the
send(),ping()andpong()callbacks reported success for a frame dropped after the connection ended but before'close'fired. They now receiveERR_STREAM_DESTROYED, like in Node.js. - Fixed: messages that arrived before a deferred
handleUpgrade(), after anawaitor an asyncverifyClient, were lost. - Fixed: in
'unexpected-response', a chunked body arrived undecoded, and a body that came after the response head was lost. - Fixed:
wsignoredproxyRequestOptions.headersof anhpagentproxy agent, so proxies that requireProxy-Authorizationresponded with 407. - Fixed: headers pushed by a
'headers'listener were missing from the 101 response (engine.io's cookies), andhandleProtocols: () => falsestill selected a protocol (graphql-ws).
node:http and undici
- Fixed:
socket.bytesWrittenwas 0 on'upgrade'and'connect'sockets innode:http. engine.io closes such sockets after a second, so a Vite or SvelteKit dev server with socket.io reloaded the page every second. - Fixed: Slack Bolt in Socket Mode reconnected forever, because
undicihad noping()export and noundici:websocket:pingandundici:websocket:pongdiagnostics channels.
Runtime#
-
Fixed: various stability issues in startup on Linux arm64, garbage collection on Linux and uncaught error reporting.
-
Fixed: the
^under a syntax error pointed too far left when double-width characters, like CJK text, came before it on the line. -
Fixed: in JIT-optimized functions,
.sort()on 16 or fewer integers returned the wrong result with a comparator like(a, b) => (a + 0.5) - (b + 0.5). Regression since v1.4.0. -
Fixed:
navigator.hardwareConcurrency,os.availableParallelism()andprocess.constrainedMemory()ignored cgroup v2 limits on Linux hosts that also have a cgroup v1 hierarchy, even justname=systemd. Bun's thread counts and GC heap sizing ignored them too. -
Fixed: an error thrown right after
let copy = valueat the top of a block could report an earlier statement's line and column, in functions with more than ~110 constants. -
Fixed:
CallSite#getColumnNumber()counted columns from 0 instead of 1. With@cspotcode/source-map-supportinstalled, readingerror.stackthrew "columnmust be greater than or equal to 0" for a frame at column 1. -
Fixed:
error.stackleft out the column of a frame at column 1, printingfile.js:3instead offile.js:3:1. -
Fixed: a stack frame at
const x = require("./x")pointed atxinstead of therequire()call. -
Fixed: an undefined JSX component was reported at its closing tag instead of its opening tag.
-
Fixed: the source preview of an uncaught error on the last line of a file with no trailing newline left that line out.
bun --hot and bun --watch#
-
Fixed: various stability issues in
bun --hotandbun --watch. -
Fixed: on macOS and Linux, timers under
bun --hotstopped firing after an uncaught exception, even after you saved a fix. Reloading also resets the exit code to 0.
bun run#
-
Fixed: various stability issues in
bun run,bunxand module resolution. -
Fixed: when running as PID 1 in a container,
bunnow exits with 128 + the signal number on Ctrl+C ordocker stop.
Transpiler#
-
Fixed: various stability issues in
Bun.Transpilerand TypeScript decorators. -
Fixed:
@dec static {}was accepted. A decorator on a class static block is now a syntax error, like in TypeScript and esbuild. -
Fixed: with standard decorators, when a class decorator returned a new class, the class name inside the class body (decorator arguments, static fields, methods) still referred to the original class.
-
Fixed: hexadecimal, octal and binary number literals above 2^53 could round to the wrong double. For example,
0xb3c8bf81fdf2247was a different number than in Node.js.Bun.JSONC.parse()had the same bug. -
Fixed:
Bun.Transpilerwithexports.eliminateleft behind imports that only the eliminated exports used. WithtrimUnusedImports: falsethe imports were kept, and TypeScript output was left with a bareimport "pkg".
bun check#
-
Fixed: a stability issue in
bun check. -
Fixed:
bun checkreported false TS2456 and TS2502 circularity errors on Slate'sCustomTypesmodule augmentation (Editor: BaseEditor & ReactEditor). That turnedEditorintoanyand hid real errors. -
Fixed: some
bun checkerrors depended on the number of threads, like a false TS2321 ("Excessive stack depth comparing types") with@sinclair/typeboxon two or more threads. -
Improved:
bun checkfollows TypeScript 7.1 and no longer reports TS2321. After 100 nested comparisons, it assumes the types are related. -
Fixed:
bun checkreported TS18046 and TS7006 errors thattscdoesn't when narrowing the result of.then()on aPromise<T> | PromiseLike<T>with"catch" in result. This affected tRPC. -
Fixed:
bun checkreported a false TS2589 ("excessively deep") on a call that left out an optional parameter whose type is infinitely deep, like TanStack Router'suseLoaderData()on recursive JSON data. -
Fixed:
bun checkreported a false TS2589 ("excessively deep") when an object built with a spread, inside a class or generic function, went through a chain of generic calls, like in ts-pattern. -
Fixed:
bun checkreported a false TS2615 ("circularly references itself in mapped type") after a type guard narrowed a class whose method takes immer'sDraft<this>. -
Fixed:
bun checkreported false TS2344, TS2345, TS2339 and TS7031 errors for some generic types that refer to each other in a cycle and useinfer. -
Fixed:
bun checkreported a false TS2322 when assigning to a union of intersections where one member holds an unresolved conditional type whose constraint isnever. -
Fixed:
bun checkreported a false TS2322 when a generic function typed through a generic alias (Id<string>) is passed to a generic wrapper and the result is assigned to the same alias. -
Fixed:
bun checkreported a false TS2554 when a class method calls an imported function that has the same name as a type parameter and a method of that class. -
Fixed:
bun checkreported an extra TS2322 on thereturnof a function whose return type annotation references itself, on top of TS2577. -
Fixed:
bun checkreported a TS7023 error thattscdoesn't for a generator without a return type that yields a variable in a loop and reassigns it from a call to itself. -
Fixed:
bun checkgave a class declared in a loop a wider type thantscwhen itsextendsexpression reads a variable the loop reassigns, which could produce a TS2322 thattscdoesn't report. -
Fixed:
bun checkreported TS7022 wheretscreports TS2339 for a destructured parameter with contextual typeunknownwhose default reads another binding, liketake({ first, second = first }). -
Fixed:
bun checkprinted{0}in place of the file names in TS18000 whentsconfig.jsonfiles extend each other circularly. -
Fixed:
bun checkmissed errors thattscreports while inferring the type of a variable that's reassigned in a loop, like TS2322 onlast = last.next = item. This also covers TS2367, TS2774, TS2349 and TS2344. -
Fixed:
bun checkmissed TS2367 on a comparison likey === 5(withy: string) in a value assigned to a loop variable that is read earlier in the loop body. -
Fixed:
bun checkmissed TS7022 when a loop reassigns its variable from a value that depends on that variable through a class property, or from afor...ofvariable, likeconst { children } = current; for (const child of children) current = child. -
Fixed:
bun checkmissed TS2502 for a setter whose parameter annotation reads the property it declares, likeset degrees(value: (Celsius | Kelvin)["degrees"]). -
Fixed:
bun checkmissed TS2589 when a generic function with a self-referential constraint was passed to a generic call, so a// @ts-expect-errorabove it was flagged as unused. -
Fixed:
bun checkmissed TS2540 and TS2704 for areadonlyproperty when its type is intersected with a mapped type that removesreadonly, likeWritable<I> & I. -
Fixed:
bun checkmissed TS2445 and TS2341 for aprotectedorprivateproperty read through an intersection of its class with a mapped type, likeRecord<"balance", unknown> & Account. -
Fixed:
bun checktook quadratic time in the number of keys of any object intsconfig.json, usuallypaths. 40,000 keys took 10.9 s and now take 0.1 s. -
Fixed:
bun checktook quadratic time when thousands ofpathsorfilesentries intsconfig.jsoneach had an error. 4,000 TS5090 errors took 35.5 s and now take 0.04 s. -
Fixed:
bun checkwas slower when the project's directory had tens of thousands of sibling files, like in$TMPDIRor a large~/code. -
Improved:
bun check -bparses a shared basetsconfig.jsononce for all projects that extend it. 1,000 projects extending 5,000pathswent from 71 s and 3.08 GB to 1.6 s and 0.62 GB. -
Improved:
bun check -bchecks small projects in parallel. It runs up to 16 projects at once instead of 4, so a monorepo of large projects uses more memory. -
Improved:
bun check --no-scriptruns the type checker even ifpackage.jsonhas acheckscript. It accepts the type checker's flags (bun check --no-script -b).bun fmtalso accepts--no-script.
bun install#
- Fixed: various stability issues in
bun install,bun update,bun outdatedandbun run.
Installing
- Fixed: with the hoisted linker, a dependency removed from
package.jsonstayed innode_modulesafterbun install, along with packages that only it depended on. - Fixed: with the isolated linker, a dependency stayed linked in
node_modulesand.binafter it was manually deleted frompackage.json, or removed withbun removeinside a workspace or while another package still depended on it. - Fixed: switching between the hoisted and isolated linkers left parts of both layouts in
node_modules, plus a.old_modules-*copy of the old tree. - Fixed: an install that was killed partway could leave a half-written package in
node_modulesthat later installs treated as installed. - Fixed: with the hoisted linker, a
file:folder outside the project (file:../plugin) couldn't load its own dependencies, and on Windows the install failed withEPERM. - Fixed:
file:folders and local tarballs were reinstalled on everybun installwith the hoisted linker. - Fixed: without a lockfile,
bun install --productionfailed when aworkspace:,link:orfile:devDependency pointed to a missing folder, like when a Docker image only contains one package from a monorepo. - Fixed:
bun installfailed withFailed to link <pkg>: EEXISTon an up-to-datenode_modulesthat's mounted read-only. - Fixed:
bun install --dry-runran the project's lifecycle scripts, anddryRun = trueinbunfig.tomlwas ignored. - Fixed:
bun install --dry-runprinted one line per dependency edge, skipped peers and ignored the platform. It now lists each package the install would add tonode_modulesonce. - Fixed:
bun install --analyzeandbun add --analyzeskipped packages that are only imported by subpath, likelodash/debounce. - Fixed: with the isolated linker, an optional dependency whose tarball couldn't be downloaded failed the install. It's now skipped.
- Fixed:
bun install <pkg>,bun linkandbun update -iexited with code 0 after printingFailed to install N packages. - Fixed: a relative
--cwdwas applied twice bybun update,bun link,bun unlinkandbun why, so they failed withENOENT. - Fixed:
bun installleft extraction directories in$TMPDIRwhen two installs shared a cache or a tarball failed to extract, which filled the disk on long-lived CI machines. - Fixed: two installs sharing a cache on NFS could delete each other's entries and fail with
ENOENT. - Fixed: with
NODE_ENV=developmentortest,bun installread.env.productioninstead of the.envfiles thatbun runreads. - Fixed:
bun --bun runonly worked for the first user on a shared machine. - Fixed: on Alpine Linux (musl),
Bun.which()returned a file that isn't executable.
Resolving versions
- Fixed:
1.0.0 || ^2.0.0resolved to1.0.0. A range that starts with an exact version was parsed as only that version. - Fixed:
bun add,bun installandbun update --latestfailed for a package whoselatestdist-tag is missing or points to an unpublished version. They now fall back to the highest release. - Fixed: dist-tags that start with a digit, such as
pkg@1beta, were parsed as version ranges. - Fixed: an auto-installed peer dependency got the newest version that one dependent allows. It now gets the newest version that all of them accept.
- Fixed: a peer dependency on
bundownloaded thebunnpm package, which is 80 to 170 MB. - Fixed: a version published after Bun cached the package's metadata couldn't be installed for up to 5 minutes.
- Fixed: in a monorepo with a lockfile, adding a dependency whose name another workspace uses for an
npm:alias requested a garbled name, such asGET /1.0.0no-deps - 404. - Fixed: editing a workspace's
package.jsonre-downloaded its GitHub and tarball dependencies and re-fetched its git dependencies, even thoughbun.lockhad already resolved them. - Fixed:
file:, tarball and git packages with nonamein theirpackage.jsongot brokenbun.lockentries. They're now named after their source. - Fixed: an
overrideskey with a prerelease range, such aspkg@>=5.0.0-alpha.0 <8, also replaced dependencies on^4. - Fixed:
bun add fooandbun update --latestinstalled a version that a version-scoped override like"foo@>=2.0.0": "1.2.0"forbids. - Fixed:
Bun.semver.satisfies()returnedtruefor versions that aren't valid semver, such as"","1.2.3.4"and"1.2.3 junk", andfalsefor1.0.0+build-1against^1.0.0. - Fixed:
Bun.semver.satisfies()andbun installparsed build metadata after a bare major (^2+x) as a second range, which allowed a different major version. - Fixed:
Bun.semver.order("2+1", "2.0.0")threw. - Fixed:
>=0.5 0was parsed as two alternatives instead of an intersection, and<xand>xmatched every version.
Hoisted and isolated linkers
- Fixed: the hoisted linker could place a package next to a version of its peer dependency that's outside the peer's range.
- Fixed: with the hoisted linker,
--filter,--production,--omit,--osand--cpuchanged where packages were placed. Switching flags could leave a workspace loading the wrong version. Placement now only depends onbun.lock. - Fixed: with the hoisted linker, a dependency nested under a package with
bundledDependenciescould load the bundled copy instead of the version it asked for. - Fixed: with the hoisted linker,
bun prunedeleted a package's bundled dependencies whenbun.lockdidn't mark them as bundled, which affectedaws-cdk-lib. - Fixed:
bun add -gdidn't link the package's binaries into the global bin folder whenlinker = "isolated"was set in~/.bunfig.toml. - Fixed: with the isolated linker, packages that are peer dependencies of each other were installed twice when different workspaces or dependencies pulled them in.
- Fixed: with the isolated linker and an
npm:alias that installs a second copy of a package,node_modules/.bun/node_modules/<name>could point at a different version after each install. - Fixed: the isolated linker replaced a symlinked
node_moduleswith a real folder. Both linkers now install through the symlink, and cleanup doesn't delete anything behind it. - Fixed: with the isolated linker on macOS and Linux, a workspace whose folder or parent folder is a symlink ended up with dangling dependency symlinks.
- Fixed: the isolated linker silently dropped symlinks inside a
file:folder dependency. Both linkers now keep symlinks that point inside the folder, copy symlinks to other project files as regular files, and skip the rest with a warning. - Fixed: with
globalStore = true, TypeScript couldn't find the project's@types/*from inside a shared package. - Fixed:
BUN_INSTALL_GLOBAL_STORE=false(alsono,offand an empty value) enabled the global store. - Fixed: symlinks to a renamed or removed workspace were left in
node_modules. - Fixed: with the hoisted linker,
bun installdidn't link a newly added workspace when the registry package of the same name and version was already innode_modules. - Fixed: on Alpine Linux, a package that ships native binaries for both libcs, such as
@anthropic-ai/claude-code, could have its command linked to the glibc binary. - Fixed: none of a package's binaries were linked if one value in its
binobject wasn't a string. - Fixed: when several packages declare the same binary, which one ended up in
.binwas arbitrary. Direct dependencies now take priority. - Fixed: a binary that's generated by the package's own install script, like a workspace CLI built in
prepare, wasn't linked into.binuntil the nextbun install. - Fixed:
bun installfailed on file systems that don't support symlinks. It now warns once and skips the.binlinks.
Lockfile and --frozen-lockfile
- Fixed: a fresh
bun installcould write a differentbun.lockdepending on which registry response arrived first. Dependencies are now resolved in a deterministic order. - Fixed:
bun cifailed in an unchanged monorepo when a workspace had a lifecycle script and a dependency onlatest, as soon as a newer version of that dependency was published. - Fixed:
bun ciappliedminimumReleaseAgeto a version already pinned inbun.lockand failed whenpackage.jsonexpressed the same version differently, such as withcatalog:. - Fixed: a direct dependency locked at a version outside its
package.jsonrange, which can happen after a bad merge, was installed anyway and passed--frozen-lockfile. - Fixed:
bun installkept a locked version that anoverridesrule forbids, which can also happen after a bad merge. It now re-resolves that entry, and--frozen-lockfilefails with an explanation. - Fixed:
bun installcould write abun.lockit couldn't read back (Failed to resolve prod dependency) when a package bundles a package that another package lists as an optional peer. - Fixed:
bun <file>.lockbprinted the lockfile from the file's directory instead of the file itself. That broke using it as a gittextconv, where git passes a temporary copy. - Fixed:
bun pm migratelefttrustedDependenciesout of thebun.lockit wrote, sobun pm untrustedlisted trusted packages as blocked. - Fixed: a
yarn.lockorpnpm-lock.yamlthat pins a GitHub dependency failed on the firstbun installand lost the pin on the second. The pinned commit is now installed, including for pnpm'stype: gitresolutions. - Fixed: migrating a Yarn v1 lockfile failed with "failed to resolve" on
link:and bare path (./dir) entries, and in a monorepo where a registry package depends on a workspace. - Fixed: migrating a Yarn v1 lockfile merged a tarball or hosted-git entry with the registry package of the same name and version, so a fork was replaced by the published package.
- Fixed: a lockfile that Bun can't migrate, like npm 6's
package-lock.json, prevented Bun from migrating ayarn.lockorpnpm-lock.yamlnext to it. Bun now warns and tries the next one. - Fixed: a single
package-lock.jsonentry from another host without anintegritymade Bun ignore the whole lockfile. The entry now keeps its version and gets its integrity from the configured registry, with a warning.
Workspaces and catalogs
- Fixed: a workspace whose
package.jsonhas nonamefailed the whole install. It's now skipped. - Fixed: depending on
latestof a sibling workspace that isn't published failed with a 404. It now links the workspace. - Fixed: a workspace was left out of the install when the root
package.jsondepends on a registry package with the same name. It now keeps itsbun.lockentry and its dependencies are installed. - Fixed:
bun add <name>inside a workspace installed the package from the registry when<name>is also a sibling workspace. It now links the workspace and writesworkspace:*. - Fixed:
bun add alias@npm:<workspace>wrote"alias": "workspace:*", which every later install rejected. - Fixed:
bun link <package>inside a workspace that hasworkspace:orcatalog:dependencies failed withWorkspace dependency "bar" not foundoris not in the catalog. - Fixed: negated
workspacesentries written as!./pkgor!pkg/were ignored, andbun run --filterand--workspacesdidn't apply negated entries. - Fixed: workspace globs skipped symlinked directories.
- Fixed:
bun update <name>rewrote non-registry entries inpackage.json, for exampleworkspace:^toworkspace:*. - Fixed:
bun run --filter <pkg> deploystill randeployandpostdeployafterpredeployfailed. It now skips them, likebun runwithout--filter.
Registries, .npmrc and bunfig.toml
- Fixed: a registry URL with a path and no trailing slash, such as
https://host/npm/repo, lost its last path segment, so every request got a 404. - Fixed:
.npmrccredentials are now matched the same way npm matches them. A//host/entry applies to every path under it, including a registry set by--registryor an environment variable. - Fixed:
~incache.dir,globalDir,globalBinDirandcafilecreated a directory literally named~. - Fixed: a relative
cafile,cache,globalDirorglobalBinDirinbunfig.tomlor.npmrcis now resolved relative to that file. A relativecafileused to only work from the project root. - Fixed: a relative
--config,--cache-diror--cafileis now resolved relative to the current working directory instead of the project root. - Fixed:
-gcommands ignoredglobalDirfrom--configand~/.bunfig.toml. - Fixed:
bun publishandbun pm whoamiignoredhttp_proxy. Those two andbun auditalso ignoredNODE_TLS_REJECT_UNAUTHORIZED=0. - Fixed: a redirected registry request ignored
NO_PROXYfor the new URL. Each redirect now picks its proxy based on its own URL, likefetch(). - Fixed: credentials embedded in a registry URL were sent percent-encoded, so
p%40sswasn't sent asp@ss. - Fixed:
BUN_CONFIG_MAX_HTTP_REQUESTSwas ignored.
Tarballs and the cache
- Fixed:
--forceand re-adding the same path or URL kept the old contents of a tarball dependency, andbun updateleft its old hash inbun.lock. - Fixed:
file:and URL tarballs were cached by path or URL, so two projects or branches with different files at the same path got each other's package. - Fixed: two registries on the same host that differ only in port, path or protocol shared cache entries.
- Fixed: uncompressed
.tarpackages of 2 MB or more, and.tgzfiles of that size served withContent-Encoding: gzip, failed to install. - Fixed:
Bun.Archive'sextract(dir, { glob })andfiles()resolved with the entries read so far on a truncated or corrupted archive. They now reject, likeextract(dir).
Git dependencies
- Improved: git dependencies now require Git 2.24 or newer.
- Fixed: a cached git dependency never picked up tags and branches pushed after it was cached. Bumping to a new tag failed with
no commit matching, andbun updatestayed on the old commit. - Fixed:
git+ssh://git@host:2222/...lost its port when Bun fell back to ssh. - Fixed: a git dependency with an ssh URL never fell back to ssh when the https attempt returned "not found".
- Fixed:
bun add name@git+ssh://host/repo.gitfailed withunrecognised dependency formatfor hosts other than GitHub. - Fixed:
deploy@host:org/repo.gitwas cloned asgit@deploy@host. - Fixed:
gitlab:,bitbucket:,gist:andsourcehut:dependencies failed to install frombun.lockon a clean machine. They're now written asgit+ssh://[email protected]/user/repo#<sha>, and existing entries still install. - Fixed: an optional git dependency that can't be cloned failed the install. It's now skipped with a warning.
- Fixed:
bun installandbun createrun from a git hook operated on the hook's repository, becauseGIT_DIRand similar variables were passed on to git. - Fixed:
bun installoverrode yourcore.sshCommandorcore.askPassfor git dependencies when it's set outside~/.gitconfig, like in~/.config/git/configor an included file. - Fixed:
submodule.recurse = truein your git config broke a git dependency that has a submodule. - Fixed: a cached clone that git can't use, because of a stale lock file or a damaged ref store, failed every install. It's now re-cloned.
Lifecycle scripts and trust
- Fixed: with the isolated linker, a package added to
trustedDependenciesafter it was installed never ran its scripts. - Fixed: a package installed through an
npm:alias couldn't be trusted consistently. It's now trusted by its real name inpackage.json,--trust,bun pm trustandbun.lock. - Fixed:
--concurrent-scriptsandconcurrentScriptshad no effect with the isolated linker, and thebunfig.tomlvalue took precedence over the flag. - Fixed:
bun pm untrusted,bun pm trustandbun pm ls --trustedused a stale lockfile instead ofpackage.json. - Fixed:
bun pm pack,bun publishandbun pm versionran lifecycle scripts withoutnode_modules/.bininPATH, so"prepack": "tsc"failed withcommand not found. - Fixed: a regression in Bun v1.4.0 where a failed lifecycle script's non-UTF-8 output, such as Latin-1 or Shift-JIS compiler errors, was printed with every invalid byte replaced by U+FFFD.
bun add, bun remove, bun update and bun outdated
- Fixed: removing the last package in a dependency group with
bun removereordered the other keys inpackage.json. - Fixed:
bun add ../pathrun from a subdirectory of the project, andbun install -g ., resolved the path from the wrong directory. - Fixed:
bun addwith a folder, tarball or git URL wrote the package intobun.locktwice whenpackage.jsonalready listed it from another source. - Fixed: the first
bun add -gon a machine edited apackage.jsonabove the global directory, such as~/package.json. - Fixed:
bun add --dry-runin an empty directory created apackage.json. - Fixed:
bun add -E react@18wrote18.--exactnow pins the range you typed to the installed version, like npm. - Fixed:
bun add,bun remove,bun updateandbun patchrewrotepackage.jsoneven when nothing changed. - Fixed: commands that rewrite
package.jsonturned control characters in strings into\x1Bor\v, which isn't valid JSON. - Fixed:
bun updateprinted(no changes)when it had rewrittenpackage.json. - Fixed:
bun update --latestdowngraded a dependency written as a dist-tag like"canary"when that tag is ahead oflatest.bun update -ino longer offers an olderlatesteither. - Fixed:
bun update --latestdowngraded acatalogentry that was ahead of thelatesttag. This affected pinned versions, ranges and dist-tags likecanary. - Fixed:
bun update --interactiverewrote ranges like>=1.0.0 <2,1.xand^1 || ^2as their first character plus the chosen version. They're now left unchanged if they already allow the chosen version. - Fixed:
bun update --exactpinnedpeerDependenciesranges, andbun update --latestnarrowed a peer range that already allows the latest version. - Fixed:
bun outdated a bonly listed packages matching both patterns. It now lists packages matching either one, and!patternexcludes. - Fixed:
bun outdatedhid dependencies that are ahead of thelatesttag. It now lists them ifbun updatewould change them.
bun pm and bun why
- Fixed:
bun pm bin -gandbun pm ls -gfailed withNo package.json was foundbefore the first global install. - Fixed:
bun pm lslisted the lockfile's packages under anode_moduleslabel. Whennode_modulesexists, it now lists what's actually installed. - Fixed:
bun why name@versionfound nothing for exact versions, scoped names and workspaces. - Fixed:
bun whyprinted a package's dependents again at every occurrence, which grew exponentially in large trees. Repeats are now marked*deduped. - Fixed:
bun pm versiondidn't create a commit or tag in a git worktree or submodule, where.gitis a file. - Fixed:
bun pm version <increment>now computes the same version asnpm version.1.0.0-rc.1plusmajoris1.0.0, not2.0.0, andv2.0.0is written as2.0.0. - Fixed:
bun pm view pkg@tagsilently usedlatestfor a dist-tag that doesn't exist. It's now an error that lists the available tags. - Fixed:
bun pm cache rmdeleted the cache directory itself, which left a dangling link when it's a symlink and failed withEACCESin a read-only parent. - Fixed:
bun pm cache rmprintedCleared 'bun install' cachewithout deleting anything when~/.bunfig.tomlset the cache directory.
bun pm pack and bun publish
- Fixed:
workspace:andcatalog:versions came frombun.lock, so they could be stale, and packing failed without a lockfile. They now come from thepackage.jsonfiles. - Fixed: a
catalog:dependency whose catalog entry isworkspace:^or anotherworkspace:form was published as-is. It's now replaced with the workspace's version. - Fixed:
bun pm packandbun publishomittedbundledDependencieswith the isolated linker and in workspaces. - Fixed:
!patternin.npmignorecouldn't re-include files below an excluded directory. - Fixed: relative
--destinationand--filenamepaths, andbun publish ./x.tgz, were resolved from the package root instead of the current directory. - Fixed:
bun publish --provenance,publishConfig.provenanceandNPM_CONFIG_PROVENANCEwere ignored, so the package was published without provenance. They're now errors until Bun supports generating it. - Fixed: a file listed under several
binnames was packed several times.
bun patch
- Fixed: a patch that adds a file in a new directory failed with
EACCESfor non-root users. - Fixed: hunks were applied by line number alone, so a patch could land in the wrong place after the file changed. They're now matched by context, like
git apply. - Fixed: a patch that appends lines to a file lost the last line when the file has no trailing newline before or after the change.
- Fixed:
bun patch --commiton an already patched package wrote.bun-tagfiles and cache paths into the patch. - Fixed:
bun patchdropped the package's bundled and nested dependencies, and--commitrecorded them as deleted. - Fixed: a
bun patchthat failed partway left a partial package, or none, innode_modules. - Fixed: a
bun patch --commitwhose patch doesn't apply, or whose install is refused, could leave the package folder,package.jsonorbun.lockchanged. Now nothing is modified until every check has passed. - Fixed: with the isolated linker,
bun patchreplacednode_modules/<pkg>with a directory that couldn't load its dependencies. - Fixed:
bun patchandbun patch --commitfailed withENOENTwhen the package is installed but no longer in the cache, for example afterbun pm cache rm. - Fixed:
bun patch --commitfailed when a patched file or the cache folder has a non-ASCII name. - Fixed:
bun installsilently skipped the parts of a patch it can't apply, like aBinary files ... differsection, and apatchedDependencieskey without a version. Each one now prints a warning.
bunx
- Fixed:
bunx pkg@latestrelinked everything on every run, and a copy more than a day old was refreshed on every run. - Fixed: several
bunx <pkg>processes started at the same time corrupted their shared install. - Fixed:
bunx <tool>with a cached copy older than a day failed when the registry returned 5xx, 429 or 408, or when a newer version's tarball couldn't be fetched. - Fixed:
bunx pkg@3failed withcould not determine executable to runin a project where another version ofpkgis installed and has no such command. - Fixed:
bunx @scope/pkgcould run another package's.binentry of the same name instead of that package's own command.
bun create
- Fixed:
bun create <local template> <dir>deleted an existing directory. It now merges, lists conflicts and only overwrites with--force. - Fixed:
git initand the first commit raced the template's tasks andbun install. The first commit now contains exactly the template. - Fixed:
bun-createtasks ran without a shell, didn't run at all in a template without dependencies, and their failures were ignored. - Fixed: the indentation of the template's
package.jsonwas lost.
Auto-install
- Fixed: timers and I/O callbacks could run inside
require(),import()andBun.resolveSync()while auto-install fetched a package. - Fixed: importing a second version (
pkg@2afterpkg@1) failed withUnexpectedwhen the package's metadata was in the disk cache. - Fixed: one failed auto-install download limited
fetch()to 4 concurrent requests for the rest of the process. - Fixed: peer dependencies of auto-installed packages failed with "Cannot find module".
- Fixed: with auto-install, an import throws "Cannot find package" when the cache or temp directory can't be written, like in a read-only container.
Windows
- Fixed: commands whose shebang uses
env -Sdidn't run. - Fixed:
.bunxshims broke when the package is on another drive. - Fixed:
bun removeleft.bunxand.exeshims behind. - Fixed:
bun installfailed in the root of a drive. - Fixed:
EINVAL: Failed to replace old lockfileon exFAT and FAT32 drives. - Fixed: renames failed while a virus scanner held a new file open. They're now retried for up to 5 seconds.
- Fixed:
bun create <local template>failed with "ENOENT: failed to copy file" when the working directory, the template or the destination has a non-ASCII character. - Fixed: the
preversion,versionandpostversionscripts ofbun pm versionran with an empty environment in a git repository.
JavaScript bundler#
-
Fixed: various stability issues in
bun build,Bun.buildplugins and macros. -
Fixed:
bun build --minifygave two default exports the same name when each was a parenthesized named function or class expression, likeexport default (function inc() {}). -
Fixed:
bun buildleft an HTML<img src>pointing to the source path without writing the file when a stylesheet inlined the same image throughurl(). -
Fixed: in source maps from
bun build --react-compiler,catchand the{after it mapped to the position oftry. -
Fixed:
bun buildsource maps mappedexport default function,export default class,export async function,abstract class,import(),require()andrequire.resolve()to the wrong column.
bun build --compile#
- Fixed: various stability issues in
bun build --compileand compiled executables.
CSS Parser#
-
Fixed: a stability issue in the CSS parser.
-
Fixed:
bun builddroppedtranslate,rotateorscaledeclared beforetransformin the same CSS rule, and merged the ones declared after it intotransform.
Dev server#
-
Fixed: a stability issue in the dev server.
-
Fixed:
bun ./index.htmlignored--port 3000and--host 0.0.0.0when written with a space instead of=. -
Fixed:
bun ./index.html --host=::1threw aRangeError. -
Fixed:
bun ./index.htmlprinted a startup error twice, such as the one for an invalid--port. -
Fixed: the underline under a CSS resolve error for
url('/fonts/a.woff2')ended 5 characters early. It now covers the wholeurl(...), and only the path incomposes: x from "./a.css".
bun test#
-
Fixed: various stability issues in
bun testandbun test --changed. -
Fixed:
bun test --update-snapshotsemptied a snapshot file when the run ended early, like with--bail. The old contents are now kept until the new ones are written. -
Fixed:
bun test --coveragecounted skipped code as covered: the right side of||,&&,??or?:on its own line, code skipped by?.,||=,&&=,??=or an unused default value, and an untakenbreak. -
Fixed:
bun test --coveragecounted the lines after anawaitoryieldas covered when the function never resumed past it.% Linescan drop after upgrading, so check yourcoverageThreshold. -
Fixed:
bun test --coveragelisted the wrongUncovered Line #sfor a function that never ran. The range could start two lines early and miss the last line. -
Fixed:
bun test --coveragereported lines that ran as uncovered, such as abreakin a block insidewhile (true), or theimportaboveclass Derived extends Base() {}. -
Fixed:
bun test --coverageignored a line with onlydefault:, orcasewith its value on the next line, in functions that ran. -
Fixed:
bun test --coveragelisted the{line instead of theelseline whenelseand{were on separate lines. Source maps gaveelsethe position of the statement after it. -
Fixed:
bun test --coveragelisted lines at the top of a file as code, comments included, when the file used a--definename whose value is a non-empty object or array. -
Fixed:
% Funcsinbun test --coveragecounted implicit constructors, class field initializers,staticblocks, and generator and async function bodies as functions. A fully tested class could show 50%. -
Fixed: the
DA:hit counts inlcov.infoweren't the number of times a line ran. A line that ran once could show 14.
Bun Shell#
-
Fixed: a stability issue in Bun Shell.
-
Fixed:
$`cmd`truncated an environment variable's value at a NUL byte..env()and.cwd()now throwERR_INVALID_ARG_VALUEfor strings containing a NUL byte. -
Fixed: in Bun Shell,
mv -noverwrote an existing destination. -
Fixed: in Bun Shell,
cp -nexited with code 1 when the destination existed.
SQL#
- Fixed: various stability issues in
Bun.SQL,bun:sqliteandnode:sqlite.
Bun.SQL connection pool
- Fixed:
idleTimeoutandmaxLifetimeclosed a connection that was running a query, was reserved, or had an opensql.begin()transaction. Now the connection finishes its work first. - Fixed: a pool that lost some of its connections didn't open new ones as long as one was still usable, so it never grew back to
max. - Fixed: a connection released to the pool inside an open transaction, such as a reservation after a manual
BEGIN, was handed to the next caller. The pool now closes it. - Fixed:
sql.close({ timeout: 0 })waited for running queries instead of closing immediately. - Fixed:
sql.close({ timeout: 2 ** 31 })overflowed the timer and killed the running queries after 1 ms. - Fixed:
sql.begin()andsql.reserve()jumped ahead of older queries that were waiting for a connection. Callers are now served in FIFO order. - Fixed:
onconnectandonclosenever fired for thesql.listen()connection. - Improved: PostgreSQL and MySQL sockets now use TCP keepalive (60 seconds), so a query fails instead of waiting forever when the server disappears.
Bun.SQL transactions
- Fixed: in
sql.begin(tx => Promise.all(tasks)), after one task failed, statements that the other tasks sent after theROLLBACKran in autocommit mode, so their rows were kept. They now reject. - Fixed: a query created on a
reservedortxhandle but first awaited afterrelease(),COMMITorROLLBACKran on a connection that could belong to another caller. It now rejects. - Fixed:
reserved.release()during a runningreserved.begin()returned the connection early, so that transaction'sCOMMITcould commit the next caller's work. - Fixed:
tx.close({ timeout })committed the transaction when its queries finished in time. It now rolls back. - Fixed: the
sql.begin()callback saw noAsyncLocalStoragestore on a cold pool, and another request's store on a busy one. It now sees its caller's. - Fixed: a result returned from a
sql.begin()ortx.savepoint()callback was returned as a plain array withoutcountandcommand. - Fixed:
sql.begin()andsql.beginDistributed()(PostgreSQL, MySQL) rejected a transaction the server had committed or prepared when the server's response and its shutdown error arrived in the same read.
Bun.SQL queries and results
- Improved: a
sql`...`call compiled by Babel's or swc's loose mode for template literals now throws aTypeError. Disable loose mode for template literals. - Fixed:
affectedRowswas alwaysnullon PostgreSQL and SQLite. It's now the number of rows written, and0for aSELECT. On MySQL,countwas0after a write. - Fixed:
WHERE id IN ${sql([])}was a syntax error on PostgreSQL and MySQL. It now matches no rows. - Fixed: on MySQL and MariaDB,
sql("db.table")quoted the whole string as a single identifier. It's now`db`.`table`, as documented. - Fixed: on MySQL,
ON DUPLICATE KEY UPDATE ${sql({ age: 31 })}was a syntax error when the clause was lowercase or split across lines. - Fixed:
sql instanceof Bun.SQLthrew aTypeError. - Improved: in TypeScript, the awaited result of a query has
count,command,affectedRows,columnsand the other result properties.
Bun.SQL connection options
- Improved:
Bun.SQLno longer reads thePASSWORDenvironment variable. UsePGPASSWORD,MYSQL_PASSWORD, the URL or thepasswordoption. - Fixed:
new SQL({ filename: "./dev.db" })withoutadaptertried to connect to PostgreSQL, andSQLITE_URL=x.dbtried to open a file namedpostgres://x.db. - Fixed: the documented
socketoption and?socket=had no effect, andmysql://user:pass@/db?socket=/paththrewInvalid URL. - Fixed: a
paththat doesn't exist was ignored, and Bun connected over TCP. The connection now fails withERR_POSTGRES_CONNECTION_REFUSEDorERR_MYSQL_CONNECTION_REFUSED. - Fixed:
PGUSERNAMEandUSERNAMEwere documented as the PostgreSQL user but ignored. - Fixed: a
mariadb://URL without a database tried to select a database namedmariadband failed with errno 1049. It no longer selects one. - Fixed: with the SQLite adapter,
?mode=rwcreated the file, and?mode=ROor{ readwrite: false }opened it read-write.
Bun.SQL with PostgreSQL
- Fixed: each distinct combination of
NULLparameters prepared another server-side statement, and none of them were ever closed, so backends grew to several GB. Each connection now keeps its 1,024 most recent statements. - Fixed: after
ALTER TABLE,DEALLOCATE ALLorDISCARD ALL, a cached statement kept failing, for example withcached plan must not change result type. Bun now re-prepares it. - Fixed:
sql.array(["a", "b"])without a type was cast toJSON[], so atext[]column stored JSON-quoted elements. Bun no longer adds a cast. - Fixed:
nullinsql.array(values, "TEXT")or"INT"became the string"null"or an error. It's now SQLNULL. - Fixed: a prepared query that returned an
int4[]orfloat4[]with aNULLor more than one dimension rejected withERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YETorERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET. - Fixed: a
Datebound totextwas stored as itstoString(), and a plain object as[object Object]. They're now stored as ISO 8601 and JSON. - Fixed: a
Datebound todateortimefailed. - Fixed: with
prepare: false, aDatewas sent as itstoString(), and an object bound tojsonbas[object Object]. - Fixed: an array or object bound to a
bool,int4,float8ortimestampparameter was coerced on the client, so[]becametrue. It's now sent as text and validated by the server. - Fixed: an Invalid Date was stored as 1970-01-01 on PostgreSQL and MySQL. The query now rejects.
- Fixed: whole numbers from 2^51 to 2^53 - 1 were sent as
float8, sonumericstored 2^52 + 1 as4503599627370500. They're now sent asint8, which Bun returns as a string. - Fixed:
COPY ... FROM STDINandCOPY ... TO STDOUTas plain queries, without a stream, now reject withERR_POSTGRES_COPY_NOT_SUPPORTED. - Fixed:
PostgresError.columnand.linepointed to a position in Bun's own source code when the server didn't send one.
Bun.SQL with MySQL and MariaDB
- Fixed: queries on the same connection were sent one at a time instead of pipelined, a regression since v1.2.22. With 30 ms of latency, four prepared queries now take 31 ms, down from 124 ms.
- Fixed: without TLS,
caching_sha2_passwordlogins failed with errno 1045 for passwords of 20 bytes or more. Fast authentication also never succeeded, so plain TCP neededallowPublicKeyRetrieval: trueon every login. - Fixed:
sha256_passwordwas documented, but every login failed with errno 1045. - Fixed: a single
INSERTwith K nullable columns could prepare up to 2^K statements on the server, which were never closed. Each connection now keeps its 256 most recent statements. - Fixed: a failed prepare was cached, so the query kept failing after a
CREATE TABLEhad made it valid. - Fixed: a query on a connection that had been idle didn't keep the process alive, so the process could exit with code 0 while the query was in flight.
- Fixed: two queries on one connection could resolve with each other's rows when a parameter's
toJSON()started a query. - Fixed: MariaDB rejected every object or array parameter with errno 1210.
- Fixed: a
Datebefore 1970 was rejected, and a prepared query lost the fractional seconds ofTIME(n)columns. - Fixed:
lastInsertRowidabove 2^53 - 1 was rounded. It's now returned as a string, or aBigIntwithbigint: true. - Fixed:
Bun.SQLthrewERR_MYSQL_UNSUPPORTED_PROTOCOL_VERSIONwhen a MySQL or MariaDB server rejected a new connection, such as when it hitmax_connections. It now throws the server's error (errno1040, "Too many connections").
Bun.SQL with SQLite
- Fixed: a valid query resolved to
[]when its text was written likeselect*from tor/*c*/select, or wasvalues (...). - Fixed:
INSERT ... SELECTandWITH ... INSERTreported acountof 0. - Fixed: a string of several statements whose first statement returns rows, such as a migration file that starts with a
PRAGMA, only ran that statement. - Fixed: a second
sql.begin()while another one was running failed withcannot start a transaction within a transaction. It now waits. - Fixed: a failed
COMMITleft the transaction open, so every latersql.begin()failed.
bun:sqlite
- Fixed:
bun:sqlitelost writes at process exit wheniterate()on a statement that writes, likeINSERT ... RETURNING, wasn't read to the end. They're now committed. An explicitBEGINis still rolled back. - Fixed:
new Database(path, { create: false })failed withSQLITE_MISUSEon an existing file, and so did{},{ readonly: false }and{ readwrite: false }. - Fixed:
{ readonly: true, create: true }opened the database read-write. It now throws aTypeError. - Fixed:
db.run("a; b; c")anddb.exec()swallowed runtime errors in every statement except the last. They now throw at the failing statement and skip the rest. - Fixed: after
ALTER TABLE, an existingStatementreturned rows with the old column names, or values under the wrong keys. - Fixed:
{ name: 1 }boundNULLto$nameby default, and{ $name: 1 }threw withstrict: true. Both forms now work in both modes. - Fixed:
db.prepare(sql, "x")ignored a binding that wasn't an array. - Fixed:
row["1"]wasundefinedforSELECT 1. - Fixed:
changesalso counted rows written by triggers, foreign key cascades and FTS5 shadow tables. It now counts the statement's own rows, likenode:sqlite. - Fixed:
breakout of astmt.iterate()loop left the cursor open, so a laterDROP TABLEfailed withdatabase table is locked. - Fixed:
db.transaction(fn)committed whenfnreturned a promise that was already rejected, which is what anasyncfunction does when it throws before its firstawait. It now rolls back. - Fixed:
stmt.run(new Date()), or aMapor aPromiseas the only argument, boundNULL. It now throws aTypeError. - Fixed: terminating a Worker during a write with
RETURNINGoutside a transaction could commit rows without their full-text (FTS) index entries.MATCHthen missed those rows.INSERT INTO ft(ft) VALUES('rebuild')repairs the index. - Fixed:
db.query()cached SQL text of any size, so a loop over multi-megabyte queries with changing text grew to 953 MB. It now peaks at 92 MB. - Improved: statements now report their native memory to the garbage collector. 20,000 dropped 200-parameter statements hold 27 MB instead of 465 MB.
- Improved:
stmt.get()stops after the first row instead of reading the whole result. Statements that write (INSERT,UPDATE,DELETE … RETURNING) still apply and commit all their changes. - Improved: rows with 63 or more columns take 310 instructions per cell, down from 1,665.
- Improved:
db.run()with a string of 80,000 statements takes 0.11 s of CPU time, down from 5.34 s. - Improved: Workers no longer share one SQLite malloc mutex. As a result,
PRAGMA hard_heap_limitandsoft_heap_limitare accepted but no longer enforced, like innode:sqlite.
node:sqlite
- Fixed:
db.close()kept the file and its-waland-shmfiles open until everyStatementSyncwas garbage collected, which causedEBUSYon Windows. - Fixed: a
Proxy(Vuereactive(), MobX) or a module namespace as named parameters boundNULLto every parameter. - Fixed:
stmt.get()onINSERT ... RETURNINGreturned a row even though the commit failed on a deferred foreign key. It now throws. - Improved:
stmt.get()on a 5-column row takes 4,077 instructions, down from 13,458.
TypeScript types#
-
Fixed:
toHaveBeenCalledOnce()was missing frombun-types. The matcher already worked inbun test. -
Fixed:
@types/buntypedBun.hash()asnumber | bigint. It always returns abigint. -
Fixed:
@types/bunaccepted aBufferorURLas the path inBun.mmap(). Both throw at runtime. -
Fixed:
@types/bunwas missingbyteLengthon instances ofBun.MD5,Bun.SHA256and the other hash classes. -
Fixed:
@types/buntyped thecpuTimefields of a subprocess'sresourceUsage()asnumberwhen they'rebigint, andFileSystemRouter'soriginasstringwhen it can benull.
منتشرشده در
Bun Blog
ادامه مطالعه




