Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2261,16 +2261,16 @@ __attribute__((minsize)) JSValue Process::emitWarning(JSC::JSGlobalObject* lexic
auto s = warning.getString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
errorInstance = createError(globalObject, !s.isEmpty() ? s : "Warning"_s);
errorInstance->putDirect(vm, vm.propertyNames->name, type, JSC::PropertyAttribute::DontEnum | 0);
// Like Node's createWarningObject: plain assignment, and only on the Error made for a string.
errorInstance->putDirect(vm, vm.propertyNames->name, type, 0);
if (!code.isUndefined()) errorInstance->putDirect(vm, builtinNames(vm).codePublicName(), code, 0);
if (!detail.isUndefined()) errorInstance->putDirect(vm, vm.propertyNames->detail, detail, 0);
} else if (warning.isCell() && warning.asCell()->type() == ErrorInstanceType) {
errorInstance = warning.getObject();
} else {
return JSValue::decode(Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "warning"_s, "string or Error"_s, warning));
}

if (!code.isUndefined()) errorInstance->putDirect(vm, builtinNames(vm).codePublicName(), code, JSC::PropertyAttribute::DontEnum | 0);
if (!detail.isUndefined()) errorInstance->putDirect(vm, vm.propertyNames->detail, detail, JSC::PropertyAttribute::DontEnum | 0);

RELEASE_AND_RETURN(scope, emitWarningErrorInstance(lexicalGlobalObject, errorInstance));
}

Expand Down
31 changes: 22 additions & 9 deletions src/jsc/bindings/ErrorCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ JSC_DEFINE_HOST_FUNCTION(NodeError_proto_toString, (JSC::JSGlobalObject * global
auto scope = DECLARE_THROW_SCOPE(vm);
auto thisVal = callFrame->thisValue().toThis(globalObject, JSC::ECMAMode::strict());

// Node's toString closes over the code rather than reading `this.code`; ours is stashed on the callee.
auto code = callFrame->jsCallee()->getDirect(vm, WebCore::builtinNames(vm).codePrivateName());
if (!code) code = JSC::jsUndefined();

auto name = thisVal.get(globalObject, vm.propertyNames->name);
RETURN_IF_EXCEPTION(scope, {});
auto code = thisVal.get(globalObject, WebCore::builtinNames(vm).codePublicName());
RETURN_IF_EXCEPTION(scope, {});
auto message = thisVal.get(globalObject, vm.propertyNames->message);
RETURN_IF_EXCEPTION(scope, {});

Expand Down Expand Up @@ -135,13 +137,23 @@ static JSC::JSObject* createErrorPrototype(JSC::VM& vm, JSC::JSGlobalObject* glo
break;
}

prototype->putDirect(vm, vm.propertyNames->name, jsString(vm, String(name)), 0);
prototype->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), jsString(vm, String(code)), 0);
prototype->putDirect(vm, vm.propertyNames->toString, JSC::JSFunction::create(vm, globalObject, 0, "toString"_s, NodeError_proto_toString, JSC::ImplementationVisibility::Private), 0);
auto* toString = JSC::JSFunction::create(vm, globalObject, 0, "toString"_s, NodeError_proto_toString, JSC::ImplementationVisibility::Private);
toString->putDirect(vm, WebCore::builtinNames(vm).codePrivateName(), jsString(vm, String(code)), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete);

// DontEnum like Node's NodeError prototype members, so `for (k in err)` sees only own properties.
prototype->putDirect(vm, vm.propertyNames->name, jsString(vm, String(name)), JSC::PropertyAttribute::DontEnum | 0);
prototype->putDirect(vm, vm.propertyNames->toString, toString, JSC::PropertyAttribute::DontEnum | 0);
Comment thread
robobun marked this conversation as resolved.
Resolved

return prototype;
}

// Node does `error.code = key` on the instance: an own, enumerable, writable property.
static JSC::ErrorInstance* putOwnCode(JSC::VM& vm, JSC::ErrorInstance* error, WTF::ASCIILiteral code)
{
error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), jsString(vm, String(code)), 0);
return error;
}

#include "ErrorCode+Data.h"

const ClassInfo ErrorCodeCache::s_info = { "ErrorCodeCache"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ErrorCodeCache) };
Expand Down Expand Up @@ -225,7 +237,7 @@ JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, E
return object;
}
}
return JSC::ErrorInstance::create(vm, structure, messageString, cause, nullptr, JSC::RuntimeType::TypeNothing, data.type, true);
return putOwnCode(vm, JSC::ErrorInstance::create(vm, structure, messageString, cause, nullptr, JSC::RuntimeType::TypeNothing, data.type, true), data.code);
}

JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, const String& message)
Expand All @@ -248,12 +260,13 @@ JSObject* createError(VM& vm, JSC::JSGlobalObject* globalObject, ErrorCode code,
if (auto* zigGlobalObject = dynamicDowncast<Zig::GlobalObject>(globalObject))
return createError(vm, zigGlobalObject, code, message, jsUndefined());

auto* structure = createErrorStructure(vm, globalObject, errors[static_cast<size_t>(code)].type, errors[static_cast<size_t>(code)].name, errors[static_cast<size_t>(code)].code);
const auto& data = errors[static_cast<size_t>(code)];
auto* structure = createErrorStructure(vm, globalObject, data.type, data.name, data.code);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
String messageString = message.isUndefined() ? String() : message.toWTFString(globalObject);
if (scope.exception() && !vm.hasPendingTerminationException())
(void)scope.tryClearException();
return JSC::ErrorInstance::create(vm, structure, messageString, JSValue(), nullptr, JSC::RuntimeType::TypeNothing, errors[static_cast<size_t>(code)].type, true);
return putOwnCode(vm, JSC::ErrorInstance::create(vm, structure, messageString, JSValue(), nullptr, JSC::RuntimeType::TypeNothing, data.type, true), data.code);
}

JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options)
Expand Down Expand Up @@ -1053,7 +1066,7 @@ JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JS
RELEASE_RETURN_IF_EXCEPTION(throwScope, {});

auto* structure = createErrorStructure(vm, globalObject, ErrorType::RangeError, "RangeError"_s, "ERR_INVALID_ARG_VALUE"_s);
auto error = JSC::ErrorInstance::create(vm, structure, builder.toString(), jsUndefined(), nullptr, JSC::RuntimeType::TypeNothing, ErrorType::RangeError, true);
auto* error = putOwnCode(vm, JSC::ErrorInstance::create(vm, structure, builder.toString(), jsUndefined(), nullptr, JSC::RuntimeType::TypeNothing, ErrorType::RangeError, true), "ERR_INVALID_ARG_VALUE"_s);
throwScope.throwException(globalObject, error);
throwScope.release();
return {};
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/S3Error.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ SYSV_ABI JSC::EncodedJSValue S3Error__toErrorInstance(const S3Error* arg0,
scope.clearException();
} else {
result->putDirect(vm, names.codePublicName(), code,
JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | 0);
JSC::PropertyAttribute::DontDelete | 0);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2871,7 +2871,7 @@ JSC::EncodedJSValue SystemError__toErrorInstanceWithInfoObject(const SystemError
auto clientData = WebCore::clientData(vm);

result->putDirect(vm, vm.propertyNames->name, jsString(vm, String("SystemError"_s)), JSC::PropertyAttribute::DontEnum | 0);
result->putDirect(vm, clientData->builtinNames().codePublicName(), jsString(vm, String("ERR_SYSTEM_ERROR"_s)), JSC::PropertyAttribute::DontEnum | 0);
result->putDirect(vm, clientData->builtinNames().codePublicName(), jsString(vm, String("ERR_SYSTEM_ERROR"_s)), 0);

info->putDirect(vm, clientData->builtinNames().codePublicName(), jsString(vm, codeString), JSC::PropertyAttribute::DontDelete | 0);

Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/webcore/streams/WebStreamsMisc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,8 @@ JSPromise* invokeCallbackReturningPromiseFast(JSGlobalObject* globalObject, JSOb

bool errorCodeIs(VM& vm, JSValue error, ASCIILiteral code)
{
// Own or inherited *data* property only (Bun's coded errors keep `code` on a per-code
// prototype); structures' stored prototypes are followed, so no getter or proxy trap runs.
// Own or inherited *data* property only; structures' stored prototypes are followed, so no
// getter or proxy trap runs.
Comment thread
robobun marked this conversation as resolved.
Resolved
JSValue codeValue;
for (JSObject* object = error ? error.getObject() : nullptr; object && !codeValue; object = object->getPrototypeDirect().getObject())
codeValue = object->getDirect(vm, WebCore::builtinNames(vm).codePublicName());
Expand Down
94 changes: 94 additions & 0 deletions test/js/node/errors/error-code-enumerable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { EventEmitter } from "node:events";
import os from "node:os";
import util from "node:util";

// Node assigns `code` (and the warning's name/code/detail) as plain enumerable own
// properties (lib/internal/errors.js, lib/internal/process/warning.js). Structured
// loggers and `JSON.stringify` rely on that.

function capture(fn: () => unknown): any {
try {
fn();
} catch (e) {
return e;
}
throw new Error("did not throw");
}

test("ERR_* code is an own enumerable property", () => {
const e = capture(() => Buffer.alloc(-1));
expect(e.code).toBe("ERR_OUT_OF_RANGE");
expect(Object.getOwnPropertyDescriptor(e, "code")).toEqual({
value: "ERR_OUT_OF_RANGE",
writable: true,
enumerable: true,
configurable: true,
});
expect(Object.keys(e)).toEqual(["code"]);
const forIn: string[] = [];
for (const k in e) forIn.push(k);
expect(forIn).toEqual(["code"]);
expect(JSON.stringify(e)).toBe('{"code":"ERR_OUT_OF_RANGE"}');
expect({ ...e }.code).toBe("ERR_OUT_OF_RANGE");
expect(util.inspect(e)).toContain("code: 'ERR_OUT_OF_RANGE'");
});

test("ERR_* code comes before the extra properties a site adds", () => {
const e = capture(() => new EventEmitter().emit("error", 42));
expect(e.code).toBe("ERR_UNHANDLED_ERROR");
expect(Object.keys(e)).toEqual(["code", "context"]);
expect(JSON.parse(JSON.stringify(e))).toEqual({ code: "ERR_UNHANDLED_ERROR", context: 42 });
});

test("ERR_INVALID_ARG_VALUE RangeError carries an own enumerable code", () => {
const e = capture(() => process.cpuUsage({ user: -1, system: 0 }));
expect(e.name).toBe("RangeError");
expect(e.code).toBe("ERR_INVALID_ARG_VALUE");
expect(Object.keys(e)).toEqual(["code"]);
});

test("ERR_* code is writable and deletable like in node", () => {
const e = capture(() => Buffer.alloc(-1));
e.code = "CUSTOM";
expect(e.code).toBe("CUSTOM");
// Node's toString closes over the original code instead of reading this.code.
expect(e.toString()).toStartWith("RangeError [ERR_OUT_OF_RANGE]: ");
delete e.code;
expect(Object.hasOwn(e, "code")).toBe(false);
expect(e.code).toBeUndefined();
expect(e.toString()).toStartWith("RangeError [ERR_OUT_OF_RANGE]: ");
});

test("ERR_SYSTEM_ERROR code is enumerable next to info, errno and syscall", () => {
const e = capture(() => os.setPriority(-1, 0));
expect(e.code).toBe("ERR_SYSTEM_ERROR");
expect(Object.keys(e).sort()).toEqual(["code", "errno", "info", "syscall"]);
});

test("warning objects expose name, code and detail as enumerable properties", async () => {
// A separate process: other test files in the same run can replace the "warning" listeners.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"--no-warnings",
"-e",
`const seen = [];
process.on("warning", w => seen.push({ keys: Object.keys(w), json: JSON.parse(JSON.stringify(w)) }));
process.emitWarning("w", { code: "MY_CODE", detail: "d" });
// Like Node, an Error passed in is emitted as is: no name, code or detail are added to it.
process.emitWarning(new Error("e"), { type: "DeprecationWarning", code: "DEP0199", detail: "d" });
process.on("exit", () => console.log(JSON.stringify(seen)));`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual([
{ keys: ["name", "code", "detail"], json: { name: "Warning", code: "MY_CODE", detail: "d" } },
{ keys: [], json: {} },
]);
expect(exitCode).toBe(0);
});
Loading