#43070·bun

Node error messages print a received -0 as 0 at several sites (Node prints -0)

Author: robobunCreated Sep 17, 2026Updated Sep 17, 2026

Node prints negative zero with its sign when it writes a received value into an error message. It uses util.inspect, or the %s of util.format, and both print -0. Bun prints 0 at the sites below. A differential run against Node v26.3.0 found them. No user reported them.

#43069 fixes the shared C++ helper JSValueToStringSafe (ERR_OUT_OF_RANGE, ERR_INVALID_ARG_VALUE and the %s codes that pass the value to it). The sites in this issue do not reach that helper with the number, so they stay wrong after #43069.

Repro

Run with bun and with node:

javascript
const crypto = require("crypto"), http = require("http"), cp = require("child_process"), http2 = require("http2");
const { StringDecoder } = require("string_decoder");
const { Readable } = require("stream");
const rows = {
  "Buffer#toString": () => Buffer.alloc(1).toString(-0),
  "StringDecoder": () => new StringDecoder(-0),
  "Readable#setEncoding": () => new Readable().setEncoding(-0),
  "crypto.randomInt": () => crypto.randomInt(-0),
  "crypto.pbkdf2Sync": () => crypto.pbkdf2Sync("a", "b", -0, 1, "sha1"),
  "process.chdir": () => process.chdir(-0),
  "http2.getUnpackedSettings": () => http2.getUnpackedSettings(-0),
  "http.validateHeaderName": () => http.validateHeaderName(-0),
  "spawnSync killSignal": () => cp.spawnSync("true", [], { killSignal: -0 }),
};
for (const [k, f] of Object.entries(rows)) {
  try { f(); console.log(k, "no throw"); } catch (e) { console.log(k, "|", e.code, "|", e.message); }
}
call Node v26.3.0 Bun 1.4.3-canary (main b64b63069c)
Buffer.alloc(1).toString(-0) Unknown encoding: -0 Unknown encoding: 0
new StringDecoder(-0) Unknown encoding: -0 Unknown encoding: 0
new Readable().setEncoding(-0) Unknown encoding: -0 Unknown encoding: 0
crypto.randomInt(-0) ... greater than the value of "min" (0). Received -0 Received 0
crypto.pbkdf2Sync("a", "b", -0, 1, "sha1") ... Received -0 Received 0
process.chdir(-0) Received type number (-0) Received type number (0)
http2.getUnpackedSettings(-0) Received type number (-0) Received type number (0)
http.validateHeaderName(-0) Header name must be a valid HTTP token ["-0"] ["0"]
child_process.spawnSync("true", [], { killSignal: -0 }) Unknown signal: -0 Unknown signal: 0

Where each one comes from

  • Unknown encoding: 0: src/jsc/bindings/JSBuffer.cpp (lines 327, 682, 1853) and src/jsc/bindings/JSStringDecoder.cpp:589 call toString on the value and pass the string to ERR::UNKNOWN_ENCODING. The overload that takes the JSValue renders more shapes like Node, but it is not a drop-in change: a value with its own toString prints differently there. https://github.com/oven-sh/bun/pull/43069#issuecomment-5714589841 compares nine values on both routes.
  • crypto.randomInt: src/runtime/node/node_crypto_binding.rs:355 converts max to i64 before it formats the message.
  • crypto.pbkdf2Sync: the f64 case of the Rust out_of_range formatter (src/bun_core/fmt.rs:3384). #40737 fixes it.
  • Received type number (0): the number case of determineSpecificType (src/jsc/bindings/ErrorCode.cpp). #38642 fixes it. src/js/node/http2.ts:3855 has a JS copy of that function with the same defect.
  • http.validateHeaderName: the table of fixed-template codes (makeSimpleErrorMessage in ErrorCode.cpp) uses ToString for each argument.
  • killSignal: ERR_UNKNOWN_SIGNAL in src/js/node/child_process.ts:1959 is a JS template string.