This issue tracks breaking changes for Bun 1.4.
Last refreshed against main on 2026-08-12. The upgrade guide that explains these changes is #36463 .
Merged
Headline
bun install / CLI
bun.lock default lockfileVersion is now 2 (install: bump default lockfileVersion to 2, gate stricter parse checks behind it #31539 ). v2 lockfiles require integrity hashes for off-registry npm tarballs and reject unsafe git .bun-tag values at parse time. Existing v0/v1 lockfiles continue to load. Older Bun versions cannot read v2 lockfiles.
bun init templates pin "typescript": "^6" (init: use TypeScript 6 in every template #33265 )
trustedDependencies and the default trusted list match the resolved package name, not the dependency alias; the default list also requires the canonical registry tarball URL; entries that only match by truncated name hash (including legacy bun.lockb entries) are no longer trusted (Hardening: input validation and bounds tightening across 28 subsystems (round 2) #31175 , install: compare trusted dependency names, not just truncated hashes #31218 , Hardening: input validation and bounds tightening across 36 subsystems (round 4) #31339 )
When Bun runs as node (the --bun shim, bunx --bun, a node symlink) it no longer auto-loads .env* files. --env-file still works and bun file.js is unchanged (cli: skip automatic .env loading when invoked as node #36610 )
Non-interactive bun update rewrites the root catalog / catalogs definitions (including with --latest), re-resolves catalog: references when run from the workspace root, and honors --recursive / --filter (install: update catalog definitions on non-interactive bun update #36304 , install: re-resolve catalog references on plain bun update from the workspace root #36379 , install: honor --recursive/--filter in non-interactive bun update; re-resolve every named-update target #36360 )
Packages kept alive only by optional-peer resolution slots are dropped from bun.lock; an existing lockfile may be rewritten once on the first install (install: drop packages held only by optional-peer resolution slots from bun.lock #35681 )
bun init with a non-TTY stdin behaves like -y instead of entering the template picker; bun update -i with a non-TTY stdin exits with an error (cli: gate bun init / update -i prompts on stdin isatty #35165 )
workspace: ranges are only honored in the root and workspace manifests; inside a downloaded package they are unresolvable like any other unknown range (Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669 ). --registry no longer forwards credentials configured for a different host (Robustness pass across install, css, ffi, crypto, spawn, shell, and node compat #36165 )
Parsers and loaders
Bundler, transpiler, and module resolution
tsconfig "jsx": "react-jsx" selects the production automatic runtime (jsx / jsxs from jsx-runtime); previously it behaved like "react-jsxdev". An explicit NODE_ENV still wins (jsx: honor tsconfig "react-jsx" vs "react-jsxdev" for the automatic runtime #34422 )
Importing a .css file at runtime gives a {} default export instead of the file's path, matching bun build (runtime: make CSS default export {} to match bun build #35163 )
tsconfig useDefineForClassFields: false is honored (instance fields are moved into the constructor); previously it was ignored (js_parser: honor tsconfig useDefineForClassFields: false #36664 )
Assigning to an imported binding is a run-time TypeError when not bundling instead of a parse error; bun build still reports an error (js_parser: make assigning to an import a run-time error outside the bundler #36046 )
bun build fails when a JS or CSS module cannot be printed (for example composes on a complex selector) instead of emitting truncated output and exiting 0 (bundler: fail the build when a module fails to print instead of emitting truncated output #37036 )
An unresolvable require() / import() inside a catch block bundles as a runtime throw instead of failing the build (bundler: downgrade unresolvable require() in catch handler to runtime throw #35659 )
Metafile imports[].path values are deterministic and match the inputs keys (bundler: make metafile import paths deterministic and match input keys #34534 )
import "." / import ".." resolve as directories (index file or package.json) instead of a same-named sibling file (resolver: resolve "." and ".." specifiers as directories, not sibling files #36969 ). Wildcard exports / imports targets get extension auto-resolution, which is looser than Node (resolver: auto-resolve extensions for wildcard exports/imports targets #36299 )
Browser target: a package's browser field mapping of a Node builtin is honored before the builtin is polyfilled (resolver: honor package.json browser field for node builtins before polyfilling #36597 ), and jsnext:main gets the same require()-falls-back-to-main treatment as module (resolver: apply the module/main auto-fallback to jsnext:main #35447 )
Bundled module namespace objects enumerate their exports in sorted order, as the spec requires (bundler: sort module namespace exports ascending to match spec #35957 ); the minifier never emits a bare $ identifier (bundler: never pick bare $ as a minified identifier #35668 )
ESM imports of built-in modules (node:*, "bun", node:process, node:module) no longer evaluate every lazy export at import time. An accessor export is read when something first binds to it, and a throwing getter (for example Bun.redis with an invalid REDIS_URL) throws from that binding instead of failing the whole import (Stop running builtin modules' lazy accessors when they are imported as ESM #37525 , Declare the "bun" module's ESM exports lazily instead of reifying the whole Bun object #37714 , Declare node:process and node:module ESM exports lazily as well #37726 )
Node.js compatibility
node:http: response.writeHeader() removed (Node DEP0063 end-of-life) (Upgrade reported Node.js version to 26.3.0 #31991 )
node:stream: read() in paused mode returns one chunk instead of concatenating the buffer (Node 26 semver-major) (Upgrade reported Node.js version to 26.3.0 #31991 )
node:tls: a server with requestCert: true and no explicit rejectUnauthorized now enforces client certificate verification (tls: apply the server's default rejectUnauthorized to incoming connections #31322 )
node:dgram: bind() on an already-bound socket and calls after close() throw synchronously (node:dgram: throw ERR_SOCKET_ALREADY_BOUND synchronously from bind() #33037 , node:dgram: throw ERR_SOCKET_DGRAM_NOT_RUNNING from socket methods after close() #33024 )
Warnings are printed in Node's shape ((node:PID) [CODE] Name: message); a user 'warning' listener runs alongside the default printer, which is registered at startup (process.listenerCount("warning") is 1, and removeAllListeners("warning") silences it); --no-warnings, --trace-warnings, --disable-warning and --redirect-warnings are honored (process: port Node.js v26.3.0 process compatibility tests and fix the gaps they surface (env exotic-object/TZ semantics, warnings pipeline + CLI flags, uncaught origin/exit codes, execve throw, threadCpuUsage/finalization/loadEnvFile, native-module identity; +26 tests) #31831 , process: register the default 'warning' listener at process creation, like Node #37344 )
process.execve() throws a SystemError on failure instead of printing and aborting; process.title defaults to argv[0] as invoked instead of "bun"; require(), import() and process.getBuiltinModule() return the same object for native modules; module.builtinModules no longer lists bun:wrap (process: port Node.js v26.3.0 process compatibility tests and fix the gaps they surface (env exotic-object/TZ semantics, warnings pipeline + CLI flags, uncaught origin/exit codes, execve throw, threadCpuUsage/finalization/loadEnvFile, native-module identity; +26 tests) #31831 )
process.reallyExit() no longer emits 'exit' listeners (process: reallyExit() should not emit 'exit' listeners #34997 )
new URL(bad) throws Node's TypeError: Invalid URL with code / input, and invalid punycode xn-- hosts are rejected for special schemes; an exception thrown inside a Node-style callback (fs, dns, pbkdf2) surfaces as an uncaughtException instead of an unhandledRejection; assert.deepStrictEqual / util.isDeepStrictEqual compare prototypes like Node (node compat batch: callback-throw dispatch, Assert class + native deep-equality parity, Intl gate + URL/buffer fallout, compile cache, watch kill-signal, profilers (+98 tests) #34660 )
util.styleText() follows the v26 API and emits no color when the target stream is not a TTY; util.inspect() brackets ArrayBuffer / typed array internals like Node ([byteLength]: 4); util.format("%s", date) prints the ISO form; vm module namespaces have a null prototype (util: node v26.3.0 compat — styleText hex/validateStream, getCallSites, regexp highlighting, vm namespace, tty.WriteStream + deepEquals fixes (test-util 70% → 83%) #34434 )
dns.lookup() (and therefore net.connect() by hostname) uses the system resolver on Linux instead of c-ares. Bun.dns.lookup() is unchanged (node:dns: use the system resolver for dns.lookup #37383 )
node:net / node:tls: accepted sockets are no longer auto-resumed, so bytes that arrive before a 'data' listener is attached are buffered like in Node; only a literal rejectUnauthorized: false disables verification; a server's rejectUnauthorized default no longer reads NODE_TLS_REJECT_UNAUTHORIZED, and requestCert must be literally true; handshakeTimeout emits 'timeout' / 'tlsClientError' instead of destroying the socket; a throwing onread callback or secureConnection listener is an uncaught exception; socket.end() sends close_notify (node:tls: sync the test suite to Node v26.3.0 and fix the gaps it surfaces (+24 tests, 155→179 of 221 upstream passing) #32630 , node:tls,node:net: follow-ups from the v26.3.0 review (error routing, manualStart reads, handshake timeout, setSecureContext) #35006 , tls: close_notify on end(), injected-socket upgrades, reject-handshake wire fix, duplex data-loss, SNI, ALPN (+14 tests, tls 81%→86%) #34598 )
node:fs: fs.open() with an options object as flags throws instead of opening read-only; fs.rm() options are validated like Node; on Windows, fs errors and process.binding("uv") carry libuv's error codes (for example -4058 for ENOENT) instead of negated CRT values (fs: add 23 Node v26.3.0 tests (node:fs 95.0% → 97.5%), Utf8Stream, and Windows errno unification #34505 ). fs.write() / writev() / readv() treat a position that is not a safe integer (including BigInt) as the current offset (fs: short write in createWriteStream overwrites head of file (NaN position coerced to 0) #36135 ); appendFile() honors an explicit flag: "w" (node:fs: honor explicit flag:'w' in appendFile instead of forcing append #36553 ); recursive fs.watch() emits 'error' for subdirectories it cannot watch instead of swallowing the failure (fs.watch: surface inotify_add_watch subtree failures as 'error' events #36415 )
node:http2: remoteSettings / localSettings are {} while connecting instead of null (node:http2: remoteSettings/localSettings return {} while connecting, not null #34358 ); the last DATA frame carries END_STREAM instead of a separate empty frame (http/http2: node v26.3.0 compat — HTTP/1 fallback + upgrade handoff, http2 session errors, perf_hooks and frame framing (+11 upstream tests) #34432 ); pushStream() failures are reported only through the callback (node:http2: pushStream failure reports only via callback, not stream 'error' #36551 )
child_process.spawn() ignores options.encoding like Node (child_process: make spawn() ignore options.encoding like Node #36050 )
node:test: a skipped suite no longer runs its callback, and { skip: true, todo: true } is a skip (node:test: run(), expectFailure, and Node v26.3.0 skip/todo semantics #34444 )
N-API: status codes on validation and failure paths match Node 26; napi_reference_ref returns 0 once the referent has been collected; napi_get_buffer_info rejects a bare ArrayBuffer (napi: align return status codes with Node.js for validation and failure paths #36805 , napi: napi_reference_ref returns 0 after the referent is collected; napi_get_buffer_info rejects bare ArrayBuffer #36850 )
WebCrypto: crypto.subtle lives on Crypto.prototype with a brand check; importing a non-JWK object as "jwk" rejects with DataError instead of throwing TypeError; an invalid key format is reported as ERR_INVALID_ARG_VALUE (webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) #34838 )
Windows: sockets are created non-inheritable, so spawned children no longer keep the parent's listeners open (Create sockets with WSA_FLAG_NO_HANDLE_INHERIT on Windows #36938 )
fetch / HTTP client
Duplicate response and request headers are combined with ", " per the Fetch spec instead of last-wins (http: combine duplicate response/request headers with ", " per the Fetch spec #31734 ). A lone empty-value header now reads as "" instead of null.
fetch() rejects the returned promise instead of throwing synchronously when option conversion throws (fetch(): reject instead of throwing synchronously when option conversion throws #33649 )
Request#clone() / Response#clone() throw when the body is disturbed or locked instead of silently returning an empty-body clone (webcore: make Request/Response clone() throw on a disturbed or locked body #33129 )
Response.redirect(url) parses and serializes the URL into Location; non-ASCII and newlines are no longer passed through verbatim (Response.redirect: parse and serialize the url into the Location header #33126 )
Truncated compressed bodies on close-delimited responses now reject instead of resolving with partial data (fetch: reject truncated compressed bodies on close-delimited responses #34922 )
Network errors reject with a TypeError instead of a plain Error (.code such as ECONNRESET is kept). After a body read fails, bodyUsed is true and a second read rejects with "Body already used" instead of the socket error (fetch: mark body disturbed when a reader fails; reject network errors as TypeError #35855 ). fetch(request) with an already-used stream body rejects with a TypeError before connecting (fetch: reject already-used Request stream bodies before connecting #36499 )
redirect: "error" only rejects 301 / 302 / 303 / 307 / 308; 300 / 304 / 305 / 306 responses are returned to the caller (fetch: only reject WHATWG redirect statuses under redirect: 'error' #36539 )
Aborting a request errors the response body (pending reads reject with AbortError) even when the body had already been fully received; previously readers drained the buffered bytes and ended cleanly (fetch: release the buffered response body and error the reader when a streaming response is aborted #32662 , fetch: error the response body stream when a fully-buffered response is aborted #35093 )
The idle timeout is an absolute deadline for receiving the response headers, so a server trickling header bytes now times out. The default (300s) is unchanged (fetch: make the idle timer an absolute deadline for the response header block #36145 )
Connection, Transfer-Encoding, Content-Encoding and Upgrade are parsed as token lists: any close token disables pooling, gzip, chunked is framed as chunked instead of rejected, identity codings are ignored (http: parse Connection/Transfer-Encoding/Content-Encoding/Upgrade as token lists #36777 ). HTTP/1.0 responses are only pooled when they say Connection: keep-alive (fetch: do not pool HTTP/1.0 responses unless they say Connection: keep-alive #37530 )
Latin-1 request header values are sent byte-for-byte per the Fetch spec instead of UTF-8 encoded (fetch: isomorphic-encode latin-1 request header values on the wire #35338 )
Bun.serve
Out-of-range port throws RangeError instead of silently clamping (Bun.serve: throw RangeError for out-of-range port instead of silently clamping #34957 )
A status outside 100..=999 is routed through error() as a 500 instead of writing an invalid status line (Bun.serve: never write a status line for a status outside 100..=999 #33400 )
Per-method route objects serve HEAD with the GET handler (Bun.serve: serve HEAD requests with the GET handler in per-method route objects #32822 )
ServerWebSocket#publish() / server.publish() return 0 / -1 on subscriber backpressure instead of always returning the payload length (Bun.serve websocket: make publish() return 0/-1 on subscriber backpressure #32889 )
WebSocket: unmasked client frames fail the connection per RFC 6455 (websocket: validate close() arguments and reject unmasked client frames #32820 )
Chunk-extension bytes are capped at 16 KiB per chunk (Bun.serve: cap chunk-extension bytes at 16 KiB per chunk #34504 )
Graceful server.stop() resolves only after in-flight requests finish and closes idle keep-alive connections itself; previously only the listener was closed (Bun.serve: gate the graceful stop() drain promise on open connections #35130 , Bun.serve: close idle connections on graceful stop(), declare closeIdleConnections() #37074 )
Requests whose Transfer-Encoding names anything other than a single final chunked (for example gzip, chunked or chunked, chunked) are rejected. node:http still accepts gzip, chunked (Bun.serve: reject Transfer-Encoding lists that name a coding other than a single final chunked #35295 )
server.upgrade() validates the opening handshake: it returns false unless Upgrade: websocket and a well-formed Sec-WebSocket-Key are present, and answers 426 Upgrade Required when Sec-WebSocket-Version is not 13 (Bun.serve: validate the WebSocket opening handshake in server.upgrade() #35298 )
Static and file routes evaluate If-Match / If-Unmodified-Since and can now answer 412 Precondition Failed (Bun.serve: evaluate If-Match / If-Unmodified-Since on static and file routes #35169 )
HTML routes no longer emit sourcemaps or .map routes when development: false; bunfig [serve.static] sourcemap overrides this (Bun.serve: don't serve sourcemaps for HTML routes in production #36982 )
requestCert / rejectUnauthorized on per-serverName tls entries are enforced for connections to that name (Bun.serve: honor requestCert/rejectUnauthorized on per-serverName tls entries #36174 ), and with http3: true they are enforced on QUIC connections too (Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669 )
ServerWebSocket#subscribe() / unsubscribe() return false on a closed socket and are now typed as returning boolean (ServerWebSocket: subscribe/unsubscribe return false on closed socket #35236 ); send() / publish() of a Blob send its bytes as a binary frame instead of the text "[object Blob]" (ServerWebSocket: send Blob bytes instead of "[object Blob]" #36032 )
WebSocket client
bun:test
Bun APIs
structuredClone / postMessage: transfer lists are validated before serialization instead of silently dropping invalid entries (Validate transfer lists before serializing instead of silently dropping invalid entries #32809 )
bun:ffi: viewSource() and new JSCallback() throw validation errors instead of returning an Error object (bun:ffi: throw validation errors from viewSource and JSCallback instead of returning them #34396 )
FileSystemRouter#match() returns null for paths not starting with / (FileSystemRouter: return null from match() when the path string does not start with '/' #34028 )
Bun.Terminal#write() returns the full input length (bytes accepted), not bytes synchronously flushed (Bun.Terminal: write() returns bytes accepted, fire drain on POSIX #34289 )
Bun.Socket#setKeepAlive(enable, ms): initialDelay is now milliseconds (was passed to the kernel as seconds, 1000x too long). setKeepAlive(true) returns true instead of false (Bun.Socket: make setKeepAlive honor documented milliseconds and fix setKeepAlive(true) returning false #34269 )
Bun.mmap() returns a view at the requested offset instead of the page-aligned offset (Bun.mmap: return view at requested offset, not page-aligned offset #34120 )
Bun.color: "ansi-16" emits real SGR codes (\x1b[91m); "ansi-256" / "hsl" / "lab" output is now parseable (color: ansi-16, ansi-256 and hsl/lab all produced unusable output #33328 , Bun.color: make 24-bit number inputs opaque instead of alpha 0 #33046 )
Bun.Cookie Expires is emitted as an IMF-fixdate (Fix Bun.Cookie Expires to emit an IMF-fixdate #32926 )
Bun.randomUUIDv7(): timestamps ≥ 2^48 or NaN throw instead of truncating (Bun.randomUUIDv7: reject timestamps >= 2^48 and NaN instead of truncating #34021 )
Bun.udpSocket({ connect: { port } }): out-of-range port throws instead of clamping to 0 (udp: reject out-of-range connect.port instead of silently clamping to 0 #34029 )
Bun.gzipSync / Bun.deflateSync with library: "libdeflate" throw TypeError for out-of-range level instead of "Out of memory" (Bun.gzipSync/deflateSync: throw invalid-argument for out-of-range libdeflate level #34114 )
Bun.YAML.parse() rejects NUL bytes with SyntaxError instead of silently truncating (yaml: reject NUL byte (U+0000) instead of silently truncating input #34852 )
Bun.redis: an invalid database segment in the connection URL throws instead of connecting to database 0 (Bun.redis: reject invalid database segment in connection URL #34039 )
Bun.spawn: argv0 and cwd containing NUL bytes throw (Bun.spawn: reject argv0 and cwd containing null bytes #33885 )
Bun.spawn / Bun.spawnSync: an already-aborted signal throws AbortError up front instead of spawning and then killing the child (Bun.spawn: throw AbortError for an already-aborted signal instead of spawning #36055 ); timeout: NaN and killSignal: 0 throw instead of being ignored (Bun.spawn: reject timeout: NaN and killSignal: 0 #35348 )
Bun.$: redirect targets that expand to multiple words are rejected (shell: reject redirect targets that expand to multiple words #34324 )
Bun.$: glob metacharacters inside interpolated values, variables and command substitutions are literal; only pattern syntax written in the template itself globs (shell: only template-literal glob tokens act as pattern syntax #31220 )
Bun.cron.parse() and in-process Bun.cron(schedule, handler) interpret schedules in local time instead of UTC, matching OS-registered jobs. Also adds a { tz } override option (cron: interpret Bun.cron.parse() and in-process schedules in local time; add { tz } option #35122 )
bun:sqlite: db.close(true) finalizes outstanding statements instead of throwing "database is locked", and a statement used after close throws "Statement has finalized"; close(false) stays graceful for prepare() statements and the query() cache is LRU (bun:sqlite: finalize outstanding prepared statements on close via sqlite3_next_stmt #36573 , bun:sqlite: keep close(false) graceful for prepare() statements, make query() cache LRU #36793 ). Empty-name columns (AS "") are kept in row objects, and stmt.columnNames after finalize() throws (bun:sqlite: keep empty-name columns; gate row-returning on sqlite3_column_count #34925 )
S3Client#list() entries expose checksumAlgorithm; the misspelled checksumAlgorithme is kept as a non-enumerable alias, so it no longer shows up in Object.keys() / JSON.stringify() output (fix(s3): expose checksumAlgorithm in list() response, keep misspelled alias non-enumerable #36502 )
Robustness passes (Robustness pass across install, css, ffi, crypto, spawn, shell, and node compat #36165 , Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669 ): Bun.spawn({ stdout: typedArray }) throws instead of aborting; FileSystemRouter no longer matches URLs shorter than the route pattern; CSS identifiers are escaped consistently on serialization; a resumed TLS 1.3 session whose client never presented a certificate reports authorized === false; Workers read process.env at runtime instead of inlining it at transpile time; bun install --verbose masks credentials. See the PR bodies for the full lists.
Input validation tightened broadly (Hardening: input validation and bounds tightening across 26 subsystems #31129 , Hardening: input validation and bounds tightening across 28 subsystems (round 2) #31175 , Hardening: input validation and bounds tightening across 31 subsystems (round 3) #31221 , Hardening: input validation and bounds tightening across 36 subsystems (round 4) #31339 , Hardening: input validation and bounds tightening across 27 subsystems (round 6) #31417 , Hardening: input validation and protocol tightening across 24 subsystems (round 7) #31495 , Hardening: input validation and bounds checking across 12 subsystems (round 8) #31559 , Hardening: input validation and compatibility fixes across 9 subsystems (round 9) #31606 , Hardening round 11: input validation, bounds checks, lifetimes #33072 , Bun.CryptoHasher: reject odd-length hex in update() #35188 , TextDecoder: reject primitive options per WebIDL dictionary conversion #35189 , node:crypto: throw (not return) validation errors from createDiffieHellman #36508 , redis: reject NaN/undefined seconds in expire() instead of sending EXPIRE key 0 #36835 , Bun.udpSocket, Bun.password: range-check numeric options before ToInt32 narrowing #36999 , Bun.openInEditor: throw when no editor is found instead of spawning "" #37210 , node:fs: bound write()'s offset by the buffer even when no length follows #37632 ). Mostly bounds checks, protocol framing, and throwing where Bun used to truncate or ignore bad input: odd-length hex in Bun.CryptoHasher#update(), primitive TextDecoder#decode() options, createDiffieHellman() errors (thrown instead of returned), Bun.password / Bun.udpSocket numeric ranges, redis.expire(key, NaN), Bun.openInEditor() with no editor, fs.write() offsets past the buffer. See individual PRs.
SQL
Under consideration
CJS / ESM / module loading
Node.js compatibility
Bun APIs
Carried over from 1.2 / 1.3
New public API (additive, not breaking)
node:sqlite (node:sqlite: implement the module and pass the Node v26.3.0 test suite #32498 )
Bun.TOML.stringify (Rewrite the TOML parser for v1.1.0 conformance #32953 )
Bun.XML.parse / Bun.XML.stringify (Add Bun.XML (parse/stringify) and an .xml loader #37048 )
Bun.isStandaloneExecutable (Add Bun.isStandaloneExecutable #32583 )
bun build --react-compiler / Bun.build({ reactCompiler: true }) (React Compiler integration #32504 )
bun build --compile --asset (compile: --asset flag + /$bunfs/ directory semantics for node:fs #36302 )
import defer (TC39 Stage 3) (Implement static import defer (TC39 Stage 3) #30975 )
process.on("memoryPressure") (Add process.on('memoryPressure') event #32594 )
fetch(url, { compress: ... }) for automatic request body compression (fetch: add automatic request body compression via compress option #32416 )
textStream() on Response / Request (fetch: implement Body.textStream() #33825 )
Bun.serve directory routes, { dir: "..." } (Bun.serve: support directory tree routes via { dir: "..." } #36156 ), and server.closeIdleConnections() (Bun.serve: close idle connections on graceful stop(), declare closeIdleConnections() #37074 )
Bun.spawn({ cgroup }) on Linux (spawn: add a cgroup option (Linux) #37466 )
bun:ffi buffer_length argument type (bun:ffi: use the engine-native FFI when available #35246 )
Bun.CSRF sessionId option (csrf: add sessionId option to bind tokens to a principal #31215 )
bun list --trusted (Add --trusted flag to bun list #32478 )
bun test --timings (bun test: --timings for duration-balanced --shard/--parallel, docs for --parallel & --isolate #36814 )
bunfig install.hoist (install: add install.hoist to disable the isolated linker's hoisted fallback directory #36972 ) and [serve.static] sourcemap (Bun.serve: don't serve sourcemaps for HTML routes in production #36982 )
--insecure-http-parser (http/http2: node v26.3.0 compat — HTTP/1 fallback + upgrade handoff, http2 session errors, perf_hooks and frame framing (+11 upstream tests) #34432 ); --tls-min-v1.x / --tls-max-v1.x flags and the crl / sessionTimeout / allowPartialTrustChain / sigalgs TLS options (node:tls: sync the test suite to Node v26.3.0 and fix the gaps it surfaces (+24 tests, 155→179 of 221 upstream passing) #32630 )
Bun.mmap offset / size options (types: add offset and size options to Bun.mmap #34573 )
TextDecoder: missing Encoding Standard encodings added (TextDecoder: add the missing Encoding Standard encodings and fix decoder conformance #32837 )
node:quic (node:quic on lsquic — Node v26 compat, HTTP/3 #32602 )
node:repl implemented, replacing the stub (node:repl: replace the stub with Node v26.3.0's REPL — v26 readline stack, acorn recoverable-parse + top-level await, completion, history, --interactive (82 vendored upstream tests) #31827 )
node:inspector: Profiler precise coverage and inspector.open() (node:inspector: implement Profiler precise coverage and inspector.open() with a DevTools-protocol server #31823 )
node:cluster: handle passing, round-robin scheduling, UDP clustering (cluster: port Node's cluster and child_process handle-passing suites (+43 upstream tests; cluster 54 → 85) and implement what they expose — round-robin fd handoff, SCHED_NONE shared handles, UDP clustering, IPC handle passing #31829 )
node:test: run() and expectFailure (node:test: run(), expectFailure, and Node v26.3.0 skip/todo semantics #34444 )
util.getCallSites(), util.convertProcessSignalToExitCode(), util.isDeepStrictEqual(a, b, skipPrototype) (util: node v26.3.0 compat — styleText hex/validateStream, getCallSites, regexp highlighting, vm namespace, tty.WriteStream + deepEquals fixes (test-util 70% → 83%) #34434 )
fs.Utf8Stream, url.fileURLToPathBuffer() (fs: add 23 Node v26.3.0 tests (node:fs 95.0% → 97.5%), Utf8Stream, and Windows errno unification #34505 )
WebCrypto: ML-DSA, ML-KEM, ChaCha20-Poly1305, encapsulate* / decapsulate*, SubtleCrypto.supports(), raw-secret / raw-public formats, KeyObject#toCryptoKey() (webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) #34838 )
http2 session.goawayCode / goawayLastStreamID (node:http2: add the goawayCode and goawayLastStreamID session getters #37550 )
ws client 'upgrade' and 'unexpected-response' events (ws: implement client 'upgrade' and 'unexpected-response' events #36272 )
V8 API: CpuProfiler (v8: implement CpuProfiler and supporting V8 APIs for @datadog/pprof #36747 ) and GCProfiler (v8: GCProfiler on a JSC HeapObserver, isStringOneByteRepresentation (+4 tests) #34550 )
This issue tracks breaking changes for Bun 1.4.
Last refreshed against
mainon 2026-08-12. The upgrade guide that explains these changes is #36463.Merged
Headline
process.versions.nodeis26.3.0,NODE_MODULE_VERSIONis147. Native addons built for Node 24 must be rebuilt.-march=haswellx64 build is dropped; SIMD is runtime-dispatched. The old non-baseline download URLs alias to the baseline artifact. This resolves Change Docker images to use non-baseline by default #12180.Temporalis enabled by default (Enable Temporal by default #32978). TheTemporalglobal andDate.prototype.toTemporalInstantnow exist;BUN_JSC_useTemporal=0turns them off.Bun.deepEquals/toEqualcompare Temporal objects by value (Compare Temporal objects by value in Bun.deepEquals and toEqual #37024).bun:ffiuses the engine-native FFI instead of compiling bindings with TinyCC (bun:ffi: use the engine-native FFI when available #35246).returns: "cstring"yields a plain string (nullfor a NULL pointer);CStringreturns a string and no longer has.ptr/.byteLength/.arrayBuffer()(bun-typesnow declarestype CString = string);napi_env/napi_valuetypes throw outsidecc();viewSource()of callbacks and the per-symbol wrapper objects are gone;dlopen()and friends throw when the JIT is disabled.cc()still uses TinyCC.bun install/ CLIbun.lockdefaultlockfileVersionis now2(install: bump default lockfileVersion to 2, gate stricter parse checks behind it #31539). v2 lockfiles require integrity hashes for off-registry npm tarballs and reject unsafe git.bun-tagvalues at parse time. Existing v0/v1 lockfiles continue to load. Older Bun versions cannot read v2 lockfiles.bun inittemplates pin"typescript": "^6"(init: use TypeScript 6 in every template #33265)trustedDependenciesand the default trusted list match the resolved package name, not the dependency alias; the default list also requires the canonical registry tarball URL; entries that only match by truncated name hash (including legacybun.lockbentries) are no longer trusted (Hardening: input validation and bounds tightening across 28 subsystems (round 2) #31175, install: compare trusted dependency names, not just truncated hashes #31218, Hardening: input validation and bounds tightening across 36 subsystems (round 4) #31339)node(the--bunshim,bunx --bun, anodesymlink) it no longer auto-loads.env*files.--env-filestill works andbun file.jsis unchanged (cli: skip automatic .env loading when invoked as node #36610)bun updaterewrites the rootcatalog/catalogsdefinitions (including with--latest), re-resolvescatalog:references when run from the workspace root, and honors--recursive/--filter(install: update catalog definitions on non-interactivebun update#36304, install: re-resolve catalog references on plainbun updatefrom the workspace root #36379, install: honor --recursive/--filter in non-interactive bun update; re-resolve every named-update target #36360)bun.lock; an existing lockfile may be rewritten once on the first install (install: drop packages held only by optional-peer resolution slots from bun.lock #35681)bun initwith a non-TTY stdin behaves like-yinstead of entering the template picker;bun update -iwith a non-TTY stdin exits with an error (cli: gate bun init / update -i prompts on stdin isatty #35165)workspace:ranges are only honored in the root and workspace manifests; inside a downloaded package they are unresolvable like any other unknown range (Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669).--registryno longer forwards credentials configured for a different host (Robustness pass across install, css, ffi, crypto, spawn, shell, and node compat #36165)Parsers and loaders
Bun.TOML: parser rewritten for TOML v1.1.0 conformance (Rewrite the TOML parser for v1.1.0 conformance #32953).Bun.TOML.parsenow throwsSyntaxError(wasBuildMessage), rejects duplicate keys, rejects integers outsideNumber.MAX_SAFE_INTEGER, rejects control characters and malformed UTF-8, and parses date/time literals as strings. Also addsBun.TOML.stringify.Bun.JSONC.parse()throwsSyntaxErrorinstead of aBuildMessage, andBun.JSONC.parse("")throws instead of returning{}(Bun.JSONC.parse: throw SyntaxError instead of BuildMessage on invalid input #35066).xmlfiles have a default loader: importing one yields the parsed document instead of the file path;--loader .xml:filerestores the old behavior (Add Bun.XML (parse/stringify) and an .xml loader #37048)Bundler, transpiler, and module resolution
"jsx": "react-jsx"selects the production automatic runtime (jsx/jsxsfromjsx-runtime); previously it behaved like"react-jsxdev". An explicitNODE_ENVstill wins (jsx: honor tsconfig "react-jsx" vs "react-jsxdev" for the automatic runtime #34422).cssfile at runtime gives a{}default export instead of the file's path, matchingbun build(runtime: make CSS default export {} to match bun build #35163)useDefineForClassFields: falseis honored (instance fields are moved into the constructor); previously it was ignored (js_parser: honor tsconfig useDefineForClassFields: false #36664)TypeErrorwhen not bundling instead of a parse error;bun buildstill reports an error (js_parser: make assigning to an import a run-time error outside the bundler #36046)bun buildfails when a JS or CSS module cannot be printed (for examplecomposeson a complex selector) instead of emitting truncated output and exiting 0 (bundler: fail the build when a module fails to print instead of emitting truncated output #37036)require()/import()inside acatchblock bundles as a runtime throw instead of failing the build (bundler: downgrade unresolvable require() in catch handler to runtime throw #35659)imports[].pathvalues are deterministic and match theinputskeys (bundler: make metafile import paths deterministic and match input keys #34534)import "."/import ".."resolve as directories (index file orpackage.json) instead of a same-named sibling file (resolver: resolve "." and ".." specifiers as directories, not sibling files #36969). Wildcardexports/importstargets get extension auto-resolution, which is looser than Node (resolver: auto-resolve extensions for wildcard exports/imports targets #36299)browserfield mapping of a Node builtin is honored before the builtin is polyfilled (resolver: honor package.json browser field for node builtins before polyfilling #36597), andjsnext:maingets the samerequire()-falls-back-to-maintreatment asmodule(resolver: apply the module/main auto-fallback to jsnext:main #35447)$identifier (bundler: never pick bare$as a minified identifier #35668)node:*,"bun",node:process,node:module) no longer evaluate every lazy export at import time. An accessor export is read when something first binds to it, and a throwing getter (for exampleBun.rediswith an invalidREDIS_URL) throws from that binding instead of failing the whole import (Stop running builtin modules' lazy accessors when they are imported as ESM #37525, Declare the "bun" module's ESM exports lazily instead of reifying the whole Bun object #37714, Declare node:process and node:module ESM exports lazily as well #37726)Node.js compatibility
node:http:response.writeHeader()removed (Node DEP0063 end-of-life) (Upgrade reported Node.js version to 26.3.0 #31991)node:stream:read()in paused mode returns one chunk instead of concatenating the buffer (Node 26 semver-major) (Upgrade reported Node.js version to 26.3.0 #31991)node:tls: a server withrequestCert: trueand no explicitrejectUnauthorizednow enforces client certificate verification (tls: apply the server's default rejectUnauthorized to incoming connections #31322)node:dgram:bind()on an already-bound socket and calls afterclose()throw synchronously (node:dgram: throw ERR_SOCKET_ALREADY_BOUND synchronously from bind() #33037, node:dgram: throw ERR_SOCKET_DGRAM_NOT_RUNNING from socket methods after close() #33024)(node:PID) [CODE] Name: message); a user'warning'listener runs alongside the default printer, which is registered at startup (process.listenerCount("warning")is1, andremoveAllListeners("warning")silences it);--no-warnings,--trace-warnings,--disable-warningand--redirect-warningsare honored (process: port Node.js v26.3.0 process compatibility tests and fix the gaps they surface (env exotic-object/TZ semantics, warnings pipeline + CLI flags, uncaught origin/exit codes, execve throw, threadCpuUsage/finalization/loadEnvFile, native-module identity; +26 tests) #31831, process: register the default 'warning' listener at process creation, like Node #37344)process.execve()throws aSystemErroron failure instead of printing and aborting;process.titledefaults to argv[0] as invoked instead of"bun";require(),import()andprocess.getBuiltinModule()return the same object for native modules;module.builtinModulesno longer listsbun:wrap(process: port Node.js v26.3.0 process compatibility tests and fix the gaps they surface (env exotic-object/TZ semantics, warnings pipeline + CLI flags, uncaught origin/exit codes, execve throw, threadCpuUsage/finalization/loadEnvFile, native-module identity; +26 tests) #31831)process.reallyExit()no longer emits'exit'listeners (process: reallyExit() should not emit 'exit' listeners #34997)new URL(bad)throws Node'sTypeError: Invalid URLwithcode/input, and invalid punycodexn--hosts are rejected for special schemes; an exception thrown inside a Node-style callback (fs,dns,pbkdf2) surfaces as anuncaughtExceptioninstead of anunhandledRejection;assert.deepStrictEqual/util.isDeepStrictEqualcompare prototypes like Node (node compat batch: callback-throw dispatch, Assert class + native deep-equality parity, Intl gate + URL/buffer fallout, compile cache, watch kill-signal, profilers (+98 tests) #34660)util.styleText()follows the v26 API and emits no color when the target stream is not a TTY;util.inspect()bracketsArrayBuffer/ typed array internals like Node ([byteLength]: 4);util.format("%s", date)prints the ISO form;vmmodule namespaces have a null prototype (util: node v26.3.0 compat — styleText hex/validateStream, getCallSites, regexp highlighting, vm namespace, tty.WriteStream + deepEquals fixes (test-util 70% → 83%) #34434)dns.lookup()(and thereforenet.connect()by hostname) uses the system resolver on Linux instead of c-ares.Bun.dns.lookup()is unchanged (node:dns: use the system resolver for dns.lookup #37383)node:net/node:tls: accepted sockets are no longer auto-resumed, so bytes that arrive before a'data'listener is attached are buffered like in Node; only a literalrejectUnauthorized: falsedisables verification; a server'srejectUnauthorizeddefault no longer readsNODE_TLS_REJECT_UNAUTHORIZED, andrequestCertmust be literallytrue;handshakeTimeoutemits'timeout'/'tlsClientError'instead of destroying the socket; a throwingonreadcallback orsecureConnectionlistener is an uncaught exception;socket.end()sendsclose_notify(node:tls: sync the test suite to Node v26.3.0 and fix the gaps it surfaces (+24 tests, 155→179 of 221 upstream passing) #32630, node:tls,node:net: follow-ups from the v26.3.0 review (error routing, manualStart reads, handshake timeout, setSecureContext) #35006, tls: close_notify on end(), injected-socket upgrades, reject-handshake wire fix, duplex data-loss, SNI, ALPN (+14 tests, tls 81%→86%) #34598)node:fs:fs.open()with an options object as flags throws instead of opening read-only;fs.rm()options are validated like Node; on Windows, fs errors andprocess.binding("uv")carry libuv's error codes (for example-4058forENOENT) instead of negated CRT values (fs: add 23 Node v26.3.0 tests (node:fs 95.0% → 97.5%), Utf8Stream, and Windows errno unification #34505).fs.write()/writev()/readv()treat apositionthat is not a safe integer (including BigInt) as the current offset (fs: short write in createWriteStream overwrites head of file (NaN position coerced to 0) #36135);appendFile()honors an explicitflag: "w"(node:fs: honor explicit flag:'w' in appendFile instead of forcing append #36553); recursivefs.watch()emits'error'for subdirectories it cannot watch instead of swallowing the failure (fs.watch: surface inotify_add_watch subtree failures as 'error' events #36415)node:http2:remoteSettings/localSettingsare{}while connecting instead ofnull(node:http2: remoteSettings/localSettings return {} while connecting, not null #34358); the last DATA frame carries END_STREAM instead of a separate empty frame (http/http2: node v26.3.0 compat — HTTP/1 fallback + upgrade handoff, http2 session errors, perf_hooks and frame framing (+11 upstream tests) #34432);pushStream()failures are reported only through the callback (node:http2: pushStream failure reports only via callback, not stream 'error' #36551)child_process.spawn()ignoresoptions.encodinglike Node (child_process: make spawn() ignore options.encoding like Node #36050)node:test: a skipped suite no longer runs its callback, and{ skip: true, todo: true }is a skip (node:test: run(), expectFailure, and Node v26.3.0 skip/todo semantics #34444)napi_reference_refreturns 0 once the referent has been collected;napi_get_buffer_inforejects a bareArrayBuffer(napi: align return status codes with Node.js for validation and failure paths #36805, napi: napi_reference_ref returns 0 after the referent is collected; napi_get_buffer_info rejects bare ArrayBuffer #36850)crypto.subtlelives onCrypto.prototypewith a brand check; importing a non-JWK object as"jwk"rejects withDataErrorinstead of throwingTypeError; an invalid key format is reported asERR_INVALID_ARG_VALUE(webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) #34838)fetch/ HTTP client", "per the Fetch spec instead of last-wins (http: combine duplicate response/request headers with ", " per the Fetch spec #31734). A lone empty-value header now reads as""instead ofnull.fetch()rejects the returned promise instead of throwing synchronously when option conversion throws (fetch(): reject instead of throwing synchronously when option conversion throws #33649)Request#clone()/Response#clone()throw when the body is disturbed or locked instead of silently returning an empty-body clone (webcore: make Request/Response clone() throw on a disturbed or locked body #33129)Response.redirect(url)parses and serializes the URL intoLocation; non-ASCII and newlines are no longer passed through verbatim (Response.redirect: parse and serialize the url into the Location header #33126)TypeErrorinstead of a plainError(.codesuch asECONNRESETis kept). After a body read fails,bodyUsedistrueand a second read rejects with "Body already used" instead of the socket error (fetch: mark body disturbed when a reader fails; reject network errors as TypeError #35855).fetch(request)with an already-used stream body rejects with aTypeErrorbefore connecting (fetch: reject already-used Request stream bodies before connecting #36499)redirect: "error"only rejects 301 / 302 / 303 / 307 / 308; 300 / 304 / 305 / 306 responses are returned to the caller (fetch: only reject WHATWG redirect statuses under redirect: 'error' #36539)AbortError) even when the body had already been fully received; previously readers drained the buffered bytes and ended cleanly (fetch: release the buffered response body and error the reader when a streaming response is aborted #32662, fetch: error the response body stream when a fully-buffered response is aborted #35093)Connection,Transfer-Encoding,Content-EncodingandUpgradeare parsed as token lists: anyclosetoken disables pooling,gzip, chunkedis framed as chunked instead of rejected,identitycodings are ignored (http: parse Connection/Transfer-Encoding/Content-Encoding/Upgrade as token lists #36777). HTTP/1.0 responses are only pooled when they sayConnection: keep-alive(fetch: do not pool HTTP/1.0 responses unless they say Connection: keep-alive #37530)Bun.serveportthrowsRangeErrorinstead of silently clamping (Bun.serve: throw RangeError for out-of-range port instead of silently clamping #34957)100..=999is routed througherror()as a 500 instead of writing an invalid status line (Bun.serve: never write a status line for a status outside 100..=999 #33400)HEADwith theGEThandler (Bun.serve: serve HEAD requests with the GET handler in per-method route objects #32822)ServerWebSocket#publish()/server.publish()return0/-1on subscriber backpressure instead of always returning the payload length (Bun.serve websocket: make publish() return 0/-1 on subscriber backpressure #32889)server.stop()resolves only after in-flight requests finish and closes idle keep-alive connections itself; previously only the listener was closed (Bun.serve: gate the graceful stop() drain promise on open connections #35130, Bun.serve: close idle connections on graceful stop(), declare closeIdleConnections() #37074)Transfer-Encodingnames anything other than a single finalchunked(for examplegzip, chunkedorchunked, chunked) are rejected.node:httpstill acceptsgzip, chunked(Bun.serve: reject Transfer-Encoding lists that name a coding other than a single final chunked #35295)server.upgrade()validates the opening handshake: it returnsfalseunlessUpgrade: websocketand a well-formedSec-WebSocket-Keyare present, and answers426 Upgrade RequiredwhenSec-WebSocket-Versionis not13(Bun.serve: validate the WebSocket opening handshake in server.upgrade() #35298)If-Match/If-Unmodified-Sinceand can now answer412 Precondition Failed(Bun.serve: evaluate If-Match / If-Unmodified-Since on static and file routes #35169).maproutes whendevelopment: false; bunfig[serve.static] sourcemapoverrides this (Bun.serve: don't serve sourcemaps for HTML routes in production #36982)requestCert/rejectUnauthorizedon per-serverNametlsentries are enforced for connections to that name (Bun.serve: honor requestCert/rejectUnauthorized on per-serverName tls entries #36174), and withhttp3: truethey are enforced on QUIC connections too (Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669)ServerWebSocket#subscribe()/unsubscribe()returnfalseon a closed socket and are now typed as returningboolean(ServerWebSocket: subscribe/unsubscribe return false on closed socket #35236);send()/publish()of aBlobsend its bytes as a binary frame instead of the text"[object Blob]"(ServerWebSocket: send Blob bytes instead of "[object Blob]" #36032)WebSocket client
close(): invalid close codes throwInvalidAccessError; reasons over 123 UTF-8 bytes throwSyntaxError(websocket: validate close() arguments and reject unmasked client frames #32820)ping()/pong()payloads over 125 bytes throwRangeError(client,ServerWebSocket, andwsshim) (WebSocket: reject ping()/pong() payloads over 125 bytes (client, Bun.serve, ws shim) #35030)Sec-WebSocket-Protocolwhen subprotocols were requested (Hardening round 11: input validation, bounds checks, lifetimes #33072)closeevent is dispatched from a queued task instead of synchronously inside the call that closed the socket (fix(WebSocket): dispatch close event as a queued task, not synchronously #27259)new WebSocket(url, { proxy })throwsSyntaxErrorfor a non-HTTP proxy scheme (for examplesocks5://) instead of sending an HTTPCONNECTto it (WebSocket: reject unsupported proxy protocols instead of sending HTTP CONNECT #35147)bun:testbun testrunsprocess.on("exit")handlers when the run finishes; previously they were skipped. A handler that callsprocess.exit(1)now fails the run (node:test: run(), expectFailure, and Node v26.3.0 skip/todo semantics #34444)jest.resetAllMocks()/vi.resetAllMocks()callmockReset()on every mock, dropping implementations set withmockImplementation()/mockReturnValue(); previously they behaved likeclearAllMocks()(bun test: make jest.resetAllMocks() reset mocks instead of clearing them #33374)toContain()compares with===like Jest:expect([-0]).toContain(0)passes andexpect([NaN]).toContain(NaN)fails (test: use strict equality in toContain to match Jest #32950)Bun.deepEquals(used bytoStrictEqualandassert.deepStrictEqual) now distinguishes boxed primitives with different contents or extra own properties, property enumerability, and own non-index properties on typed arrays; a few loose-mode boxed-primitive cases changed as well (util: node v26.3.0 compat — styleText hex/validateStream, getCallSites, regexp highlighting, vm namespace, tty.WriteStream + deepEquals fixes (test-util 70% → 83%) #34434)Bun APIs
structuredClone/postMessage: transfer lists are validated before serialization instead of silently dropping invalid entries (Validate transfer lists before serializing instead of silently dropping invalid entries #32809)bun:ffi:viewSource()andnew JSCallback()throw validation errors instead of returning anErrorobject (bun:ffi: throw validation errors from viewSource and JSCallback instead of returning them #34396)FileSystemRouter#match()returnsnullfor paths not starting with/(FileSystemRouter: return null from match() when the path string does not start with '/' #34028)Bun.Terminal#write()returns the full input length (bytes accepted), not bytes synchronously flushed (Bun.Terminal: write() returns bytes accepted, fire drain on POSIX #34289)Bun.Socket#setKeepAlive(enable, ms):initialDelayis now milliseconds (was passed to the kernel as seconds, 1000x too long).setKeepAlive(true)returnstrueinstead offalse(Bun.Socket: make setKeepAlive honor documented milliseconds and fix setKeepAlive(true) returning false #34269)Bun.mmap()returns a view at the requestedoffsetinstead of the page-aligned offset (Bun.mmap: return view at requested offset, not page-aligned offset #34120)Bun.color:"ansi-16"emits real SGR codes (\x1b[91m);"ansi-256"/"hsl"/"lab"output is now parseable (color: ansi-16, ansi-256 and hsl/lab all produced unusable output #33328, Bun.color: make 24-bit number inputs opaque instead of alpha 0 #33046)Bun.CookieExpiresis emitted as an IMF-fixdate (Fix Bun.Cookie Expires to emit an IMF-fixdate #32926)Bun.randomUUIDv7(): timestamps ≥ 2^48 orNaNthrow instead of truncating (Bun.randomUUIDv7: reject timestamps >= 2^48 and NaN instead of truncating #34021)Bun.udpSocket({ connect: { port } }): out-of-range port throws instead of clamping to 0 (udp: reject out-of-range connect.port instead of silently clamping to 0 #34029)Bun.gzipSync/Bun.deflateSyncwithlibrary: "libdeflate"throwTypeErrorfor out-of-rangelevelinstead of"Out of memory"(Bun.gzipSync/deflateSync: throw invalid-argument for out-of-range libdeflate level #34114)Bun.YAML.parse()rejects NUL bytes withSyntaxErrorinstead of silently truncating (yaml: reject NUL byte (U+0000) instead of silently truncating input #34852)Bun.redis: an invalid database segment in the connection URL throws instead of connecting to database 0 (Bun.redis: reject invalid database segment in connection URL #34039)Bun.spawn:argv0andcwdcontaining NUL bytes throw (Bun.spawn: reject argv0 and cwd containing null bytes #33885)Bun.spawn/Bun.spawnSync: an already-abortedsignalthrowsAbortErrorup front instead of spawning and then killing the child (Bun.spawn: throw AbortError for an already-aborted signal instead of spawning #36055);timeout: NaNandkillSignal: 0throw instead of being ignored (Bun.spawn: reject timeout: NaN and killSignal: 0 #35348)Bun.$: redirect targets that expand to multiple words are rejected (shell: reject redirect targets that expand to multiple words #34324)Bun.$: glob metacharacters inside interpolated values, variables and command substitutions are literal; only pattern syntax written in the template itself globs (shell: only template-literal glob tokens act as pattern syntax #31220)Bun.cron.parse()and in-processBun.cron(schedule, handler)interpret schedules in local time instead of UTC, matching OS-registered jobs. Also adds a{ tz }override option (cron: interpret Bun.cron.parse() and in-process schedules in local time; add { tz } option #35122)bun:sqlite:db.close(true)finalizes outstanding statements instead of throwing "database is locked", and a statement used after close throws "Statement has finalized";close(false)stays graceful forprepare()statements and thequery()cache is LRU (bun:sqlite: finalize outstanding prepared statements on close via sqlite3_next_stmt #36573, bun:sqlite: keep close(false) graceful for prepare() statements, make query() cache LRU #36793). Empty-name columns (AS "") are kept in row objects, andstmt.columnNamesafterfinalize()throws (bun:sqlite: keep empty-name columns; gate row-returning on sqlite3_column_count #34925)S3Client#list()entries exposechecksumAlgorithm; the misspelledchecksumAlgorithmeis kept as a non-enumerable alias, so it no longer shows up inObject.keys()/JSON.stringify()output (fix(s3): expose checksumAlgorithm in list() response, keep misspelled alias non-enumerable #36502)Bun.spawn({ stdout: typedArray })throws instead of aborting;FileSystemRouterno longer matches URLs shorter than the route pattern; CSS identifiers are escaped consistently on serialization; a resumed TLS 1.3 session whose client never presented a certificate reportsauthorized === false; Workers readprocess.envat runtime instead of inlining it at transpile time;bun install --verbosemasks credentials. See the PR bodies for the full lists.Bun.CryptoHasher#update(), primitiveTextDecoder#decode()options,createDiffieHellman()errors (thrown instead of returned),Bun.password/Bun.udpSocketnumeric ranges,redis.expire(key, NaN),Bun.openInEditor()with no editor,fs.write()offsets past the buffer. See individual PRs.SQL
DATETIME/TIMESTAMPcolumns are decoded as UTC to match the encoder. Previously they were decoded as local time and round-tripped shifted by the UTC offset (mysql: decode DATETIME/TIMESTAMP as UTC to match the UTC-based encode #31212)'infinity'/'-infinity'values ofdate/timestamp/timestamptzdecode to the NumberInfinity/-Infinityinstead of anInvalid Date(sql(postgres): decode 'infinity'::date/timestamp to the Number ±Infinity #35121)JSONcolumns and JSON function results decode to objects instead of strings, as they already did on MySQL (sql(mysql): negotiate MariaDB extended type info so JSON columns parse into objects #37130)connectionTimeoutbounds the whole handshake instead of being re-armed on every packet; a second authentication request from the server is rejected (sql(mysql,postgres): reject duplicate auth requests; make connectionTimeout an absolute handshake deadline #36308)PGSSLMODE/PG_SSLMODEfrom the environment; a URL?sslmode=still wins (sql(postgres): honour PGSSLMODE from the environment #36840).?ssl=/?ssl-mode=spellings are accepted andtls: { caFile }enables verification likecadoes (Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669)Under consideration
CJS / ESM / module loading
.cjs/"type": "commonjs"rejects ESMexportsyntax and top-levelawait)thisisundefinedin ES modules)__esModule#9267 (remove the__esModuleworkaround)pkg/package.jsonis subject to theexportsmap like in Node)awaitinstead of hanging)Node.js compatibility
process.envassignments are coerced to strings on every path; assigning a Symbol throws)net.SocketAddress/net.BlockListstrict address parsing and Node API alignment)node:http/http2hardening: a handler that throws before writing produces a 500 instead of an empty 200; h2 request pseudo-headers are enforced)Bun APIs
FileSink#write()returns0andflush()returnsundefinedafterend())bun:sqlitethrows when aStatementis reused whileiterate()is live)bun:sqliteexec()throws on a step-time error in a non-final statement instead of continuing)Sec-WebSocket-Extensions)fetch()network errors reject asTypeError("fetch failed")withcauseand errno-style codes)Bun.semverandbun installparse space-separated comparators as an intersection, like node-semver)process.versions.picohttpparser)bun:testdeprecate legacy Jest matcher aliases)packages/bun-types/deprecated.d.ts(readableStreamToBytes/Blob/Text/JSON,keyFile/certFile/caFile, etc.)Carried over from 1.2 / 1.3
--bundefault, introduce--node#4464 (make--bunthe default, introduce--node)New public API (additive, not breaking)
node:sqlite(node:sqlite: implement the module and pass the Node v26.3.0 test suite #32498)Bun.TOML.stringify(Rewrite the TOML parser for v1.1.0 conformance #32953)Bun.XML.parse/Bun.XML.stringify(Add Bun.XML (parse/stringify) and an .xml loader #37048)Bun.isStandaloneExecutable(Add Bun.isStandaloneExecutable #32583)bun build --react-compiler/Bun.build({ reactCompiler: true })(React Compiler integration #32504)bun build --compile --asset(compile: --asset flag + /$bunfs/ directory semantics for node:fs #36302)import defer(TC39 Stage 3) (Implement staticimport defer(TC39 Stage 3) #30975)process.on("memoryPressure")(Add process.on('memoryPressure') event #32594)fetch(url, { compress: ... })for automatic request body compression (fetch: add automatic request body compression viacompressoption #32416)textStream()onResponse/Request(fetch: implement Body.textStream() #33825)Bun.servedirectory routes,{ dir: "..." }(Bun.serve: support directory tree routes via{ dir: "..." }#36156), andserver.closeIdleConnections()(Bun.serve: close idle connections on graceful stop(), declare closeIdleConnections() #37074)Bun.spawn({ cgroup })on Linux (spawn: add acgroupoption (Linux) #37466)bun:ffibuffer_lengthargument type (bun:ffi: use the engine-native FFI when available #35246)Bun.CSRFsessionIdoption (csrf: add sessionId option to bind tokens to a principal #31215)bun list --trusted(Add--trustedflag tobun list#32478)bun test --timings(bun test: --timings for duration-balanced --shard/--parallel, docs for --parallel & --isolate #36814)install.hoist(install: add install.hoist to disable the isolated linker's hoisted fallback directory #36972) and[serve.static] sourcemap(Bun.serve: don't serve sourcemaps for HTML routes in production #36982)--insecure-http-parser(http/http2: node v26.3.0 compat — HTTP/1 fallback + upgrade handoff, http2 session errors, perf_hooks and frame framing (+11 upstream tests) #34432);--tls-min-v1.x/--tls-max-v1.xflags and thecrl/sessionTimeout/allowPartialTrustChain/sigalgsTLS options (node:tls: sync the test suite to Node v26.3.0 and fix the gaps it surfaces (+24 tests, 155→179 of 221 upstream passing) #32630)Bun.mmapoffset/sizeoptions (types: add offset and size options to Bun.mmap #34573)TextDecoder: missing Encoding Standard encodings added (TextDecoder: add the missing Encoding Standard encodings and fix decoder conformance #32837)node:quic(node:quic on lsquic — Node v26 compat, HTTP/3 #32602)node:replimplemented, replacing the stub (node:repl: replace the stub with Node v26.3.0's REPL — v26 readline stack, acorn recoverable-parse + top-level await, completion, history, --interactive (82 vendored upstream tests) #31827)node:inspector: Profiler precise coverage andinspector.open()(node:inspector: implement Profiler precise coverage and inspector.open() with a DevTools-protocol server #31823)node:cluster: handle passing, round-robin scheduling, UDP clustering (cluster: port Node's cluster and child_process handle-passing suites (+43 upstream tests; cluster 54 → 85) and implement what they expose — round-robin fd handoff, SCHED_NONE shared handles, UDP clustering, IPC handle passing #31829)node:test:run()andexpectFailure(node:test: run(), expectFailure, and Node v26.3.0 skip/todo semantics #34444)util.getCallSites(),util.convertProcessSignalToExitCode(),util.isDeepStrictEqual(a, b, skipPrototype)(util: node v26.3.0 compat — styleText hex/validateStream, getCallSites, regexp highlighting, vm namespace, tty.WriteStream + deepEquals fixes (test-util 70% → 83%) #34434)fs.Utf8Stream,url.fileURLToPathBuffer()(fs: add 23 Node v26.3.0 tests (node:fs 95.0% → 97.5%), Utf8Stream, and Windows errno unification #34505)encapsulate*/decapsulate*,SubtleCrypto.supports(),raw-secret/raw-publicformats,KeyObject#toCryptoKey()(webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) #34838)http2session.goawayCode/goawayLastStreamID(node:http2: add the goawayCode and goawayLastStreamID session getters #37550)wsclient'upgrade'and'unexpected-response'events (ws: implement client 'upgrade' and 'unexpected-response' events #36272)CpuProfiler(v8: implement CpuProfiler and supporting V8 APIs for @datadog/pprof #36747) andGCProfiler(v8: GCProfiler on a JSC HeapObserver, isStringOneByteRepresentation (+4 tests) #34550)