diff --git a/src/jsc/bindings/ErrorCode.cpp b/src/jsc/bindings/ErrorCode.cpp index e3818ae41c34..5f6a2b962719 100644 --- a/src/jsc/bindings/ErrorCode.cpp +++ b/src/jsc/bindings/ErrorCode.cpp @@ -14,6 +14,7 @@ #include "JavaScriptCore/ErrorInstance.h" #include "JavaScriptCore/JSString.h" #include "JavaScriptCore/JSType.h" +#include "JavaScriptCore/MathCommon.h" #include "JavaScriptCore/Symbol.h" #include "wtf/Assertions.h" #include "wtf/Vector.h" @@ -351,6 +352,12 @@ void JSValueToStringSafe(JSC::JSGlobalObject* globalObject, MessageBuilder& buil { ASSERT(!arg.isEmpty()); if (!arg.isCell()) { + // util.inspect and util.format's %s both print -0 with its sign. ToString drops it. + // https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util/inspect.js#L2191-L2197 + if (arg.isDouble() && JSC::isNegativeZero(arg.asDouble())) { + builder.append("-0"_s); + return; + } builder.append(arg.toWTFStringForConsole(globalObject)); return; } diff --git a/test/js/node/errors/error-code-messages.test.ts b/test/js/node/errors/error-code-messages.test.ts index cbb3d1222d79..4b66d730506b 100644 --- a/test/js/node/errors/error-code-messages.test.ts +++ b/test/js/node/errors/error-code-messages.test.ts @@ -1,7 +1,9 @@ import { expect, test } from "bun:test"; import child_process from "node:child_process"; +import { generateKeyPairSync } from "node:crypto"; import http from "node:http"; -import { Readable } from "node:stream"; +import { createHistogram } from "node:perf_hooks"; +import { Readable, Writable } from "node:stream"; import tls from "node:tls"; import zlib from "node:zlib"; @@ -35,3 +37,34 @@ test("table-driven ERR_* codes keep their exact messages", () => { "ERR_METHOD_NOT_IMPLEMENTED | Error | The _read() method is not implemented", ); }); + +// Node renders the value in these messages with util.inspect, or with the %s of util.format. +// Both print negative zero with its sign. Node v26.3.0 prints these exact messages. +test("a value of -0 keeps its sign in an error message", () => { + expect({ + // ERR_OUT_OF_RANGE thrown from C++, with numeric bounds and with a range string. + readUIntBE: capture(() => Buffer.alloc(8).readUIntBE(0, -0)), + percentile: capture(() => createHistogram().percentile(-0)), + // ERR_OUT_OF_RANGE thrown from JS ($ERR_OUT_OF_RANGE). + figures: capture(() => createHistogram({ figures: -0 })), + // ERR_INVALID_ARG_VALUE. + paramEncoding: capture(() => generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: -0 as any })), + // %s codes. + setDefaultEncoding: capture(() => new Writable().setDefaultEncoding(-0 as any)), + readableStreamFrom: capture(() => ReadableStream.from(-0 as any)), + // Positive zero has no sign. + positiveZero: capture(() => Buffer.alloc(8).readUIntBE(0, 0)), + }).toEqual({ + readUIntBE: + 'ERR_OUT_OF_RANGE | RangeError | The value of "byteLength" is out of range. It must be >= 1 and <= 6. Received -0', + percentile: + 'ERR_OUT_OF_RANGE | RangeError | The value of "percentile" is out of range. It must be > 0 && <= 100. Received -0', + figures: + 'ERR_OUT_OF_RANGE | RangeError | The value of "options.figures" is out of range. It must be >= 1 && <= 5. Received -0', + paramEncoding: "ERR_INVALID_ARG_VALUE | TypeError | The property 'options.paramEncoding' is invalid. Received -0", + setDefaultEncoding: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: -0", + readableStreamFrom: "ERR_ARG_NOT_ITERABLE | TypeError | -0 must be iterable", + positiveZero: + 'ERR_OUT_OF_RANGE | RangeError | The value of "byteLength" is out of range. It must be >= 1 and <= 6. Received 0', + }); +});