Skip to content

node errors: make code an own enumerable property on ERR_* errors and warnings - #41737

Open
robobun wants to merge 6 commits into
mainfrom
robobun/f4e8da95/enumerable-error-code
Open

robobun wants to merge 6 commits into
mainfrom
robobun/f4e8da95/enumerable-error-code

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • code on every ERR_* error is not an own enumerable property. Object.keys(err) is [], JSON.stringify(err) is {}, { ...err }.code is undefined. Node shows { code: 'ERR_OUT_OF_RANGE' } in each case. process.emitWarning() objects (name/code/detail), ERR_SYSTEM_ERROR and S3Error have the same gap.
  • The cause: createErrorPrototype (src/jsc/bindings/ErrorCode.cpp:138) put code on the cached per-code prototype, never on the instance. for (k in err) also leaked name, code, toString from there. The other three sites passed DontEnum.

Fix

  • putOwnCode puts the table's code on the instance as a plain own property at the three ErrorInstance::create sites in ErrorCode.cpp. The shared prototype keeps name and toString, now DontEnum. So for...in yields code alone and delete err.code leaves undefined, as in Node.
  • Process::emitWarning assigns name/code/detail without DontEnum, and only on the Error it creates for a string warning. An Error passed in is emitted untouched, like Node's createWarningObject path.
  • code on ERR_SYSTEM_ERROR and on S3Error drops DontEnum.
  • Verified: test/js/node/errors/error-code-enumerable.test.ts (6 of 6 fail on 1.4.3, all pass here). More suites in the notes.

Background

  • Every ERR_* error, thrown from C++, Rust or built-in JS, goes through ErrorCodeCache::createError. It caches one Structure per code. Its prototype sits between the instance and TypeError.prototype (or RangeError, ...) and carries name and a Node-style toString (Name [CODE]: message). Node's lib/internal/errors.js has the same shape and does error.code = key on the instance.
  • putDirect(vm, name, value, 0) creates the same writable, enumerable, configurable property as a plain JS assignment. PropertyAttribute::DontEnum hides it from Object.keys, JSON.stringify, spread and for...in.
Notes
  • This supersedes node errors: make .code an own property on ERR_* errors #35926 (July, conflicting with main). That PR also dropped DontDelete from the errno-family SystemError properties. This one leaves those alone to keep the diff to the enumerability question.
  • Before this change emitWarning(anError, { code, detail }) attached a hidden code/detail to the caller's Error. Node does not touch it, so neither does Bun now.
  • The code string is minted from the table literal on each creation instead of read back from the prototype. A user can reach the prototype with Object.getPrototypeOf(err) and delete or reassign properties on it, so reading from it is not safe.
  • The uncaught-error printer output does not change: it prints code: "ERR_..." from the instance. util.inspect(err) now appends { code: 'ERR_...' } like Node.
  • e.toString() still returns RangeError [ERR_OUT_OF_RANGE]: ... through the prototype toString. That function carries the code under a private name and reads it from the callee, like Node's closure over key, so the output survives delete err.code or a reassigned err.code.
  • All internal Process::emitWarning callers (crypto deprecations, process.binding('uv'), MessagePort, timers, IPC, unhandled rejections) pass a string warning, so moving the code/detail assignment into the string branch does not change them.
  • Other suites run with the debug build: test/js/node/{errors,events,util,v8,url,os,assert}, test/js/node/buffer.test.js, test/js/node/process/process.test.js, test/js/bun/util/inspect.test.js, test/js/bun/util/inspect-error.test.js, test/js/bun/test/expect.test.js, test/js/bun/s3/s3-argument-validation.test.ts, test/js/web/console, test/js/node/util/node-inspect-tests, and the vendored test-process-warning.js, test-process-warnings.mjs, test-process-emitwarning.js, test-event-emitter-errors.js, test-common-expect-warning.js, test-assert-deep-with-error.js. The only failures were pre-existing and environmental (process.env.USER unset in this container, timing tests under ASAN).
  • Repro from the report:
import util from 'node:util'; import { EventEmitter } from 'node:events';
const show = (l, e) => console.log(l, JSON.stringify({ code: e.code, keys: Object.keys(e), json: JSON.stringify(e), spread: ({ ...e }).code, inspectHasCode: util.inspect(e).includes(String(e.code)) }));
try { Buffer.alloc(-1); } catch (e) { show('Buffer.alloc(-1)   ', e); }
try { new EventEmitter().emit('error', 42); } catch (e) { show('ERR_UNHANDLED_ERROR', e); }
process.on('warning', (w) => show('warning            ', w)); process.emitWarning('w', { code: 'MY_CODE', detail: 'd' });

Before: keys: [], json: "{}", inspectHasCode: false on all three. After: identical to Node (["code"], ["code","context"], ["name","code","detail"]).


[human-review] gate passed · iteration 1 · 5 files touched

fails on main (without fix)
ASAN without fix: 6 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/errors/error-code-enumerable.test.ts
bun test v1.4.3 (f42e98025)

test/js/node/errors/error-code-enumerable.test.ts:
18 | }
19 | 
20 | test("ERR_* code is an own enumerable property", () => {
21 |   const e = capture(() => Buffer.alloc(-1));
22 |   expect(e.code).toBe("ERR_OUT_OF_RANGE");
23 |   expect(Object.getOwnPropertyDescriptor(e, "code")).toEqual({
                                                          ^
error: expect(received).toEqual(expected)

- {
-   "configurable": true,
-   "enumerable": true,
-   "value": "ERR_OUT_OF_RANGE",
-   "writable": true,
- }
+ undefined

- Expected  - 6
+ Received  + 1

      at <anonymous> (/workspace/bun/test/js/node/errors/error-code-enumerable.test.ts:23:54)
(fail) ERR_* code is an own enumerable property [23.58ms]
36 | });
37 | 
38 | test("ERR_* code comes before the extra properties a site adds", () => {
39 |   const e = capture(() => new EventEmitter().emit("error", 42));
40 |   expect(e.code).toBe("ERR_UNHANDLED_ERROR");
41 |   expect(Object.keys(e)).toEqual(["code", "conte
... (truncated)

release without fix: 6 FAILED
bun test v1.4.3-canary.1 (f42e98025)

test/js/node/errors/error-code-enumerable.test.ts:
18 | }
19 | 
20 | test("ERR_* code is an own enumerable property", () => {
21 |   const e = capture(() => Buffer.alloc(-1));
22 |   expect(e.code).toBe("ERR_OUT_OF_RANGE");
23 |   expect(Object.getOwnPropertyDescriptor(e, "code")).toEqual({
                                                          ^
error: expect(received).toEqual(expected)

- {
-   "configurable": true,
-   "enumerable": true,
-   "value": "ERR_OUT_OF_RANGE",
-   "writable": true,
- }
+ undefined

- Expected  - 6
+ Received  + 1

      at <anonymous> (/workspace/bun/test/js/node/errors/error-code-enumerable.test.ts:23:54)
(fail) ERR_* code is an own enumerable property [0.38ms]
36 | });
37 | 
38 | test("ERR_* code comes before the extra properties a site adds", () => {
39 |   const e = capture(() => new EventEmitter().emit("error", 42));
40 |   expect(e.code).toBe("ERR_UNHANDLED_ERROR");
41 |   expect(Object.keys(e)).toEqual(["code", "context"]);
                              ^
error: expect(received).toEqual(expected)

  [
-   "code",
    "context",
  ]

- Expected  - 1
+ Received  + 0

      at <anonymous> (/
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/errors/error-code-enumerable.test.ts
bun test v1.4.3 (f42e98025)

test/js/node/errors/error-code-enumerable.test.ts:
(pass) ERR_* code is an own enumerable property [144.92ms]
(pass) ERR_* code comes before the extra properties a site adds [25.80ms]
(pass) ERR_INVALID_ARG_VALUE RangeError carries an own enumerable code [5.31ms]
(pass) ERR_* code is writable and deletable like in node [5.98ms]
(pass) ERR_SYSTEM_ERROR code is enumerable next to info, errno and syscall [6.19ms]
(pass) warning objects expose name, code and detail as enumerable properties [428.33ms]

 6 pass
 0 fail
 22 expect() calls
Ran 6 tests across 1 file. [2.65s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     83a4fe6e76
  features     lto, baseline

24 deps, 131 codegen, 2175 objects in 1341ms

ninja: Entering directory `/workspace/bun/build/release'
[1/2863] mkdir obj
[2/2863] mkdir pch
[3/2863] mkdir codegen
[4/2863] mkdir stamps
[5/2863] gen ErrorCode+*.h
[6/2863] fetch mimalloc
[mimalloc] up to date
[7/2863] fetch tinycc
[tinycc] up to date
[8/2863] fetch WebKit
[WebKit] up to date
[9/2863] gen bindgenv2
[10/2863] fetch icu
[icu] up to date
[11/2863] fetch zlib
[zlib] up to date
[12/2863] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[13/2863] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[14/2863] host-cc deps/tinycc/host-obj/conftest.c.o
[15/2863] install /workspace/bun
bun install v1.4.3-canary.1 (f42e98025)

Checked 22 installs across 61 packages (no changes) [145.00ms]
[16/2863] cc obj/src/jsc/bindings/sqlite/sqlite3.c.o
[17/2863] gen .bind.ts → GeneratedBindings.cpp
[18/2863] gen lut ArrayConstructor.lut.h
[19/2863] gen CombinedDomains.json
[
... (truncated)
diff hotspot
src/jsc/bindings/BunProcess.cpp                   |  8 +-
 src/jsc/bindings/ErrorCode.cpp                    | 32 +++++---
 src/jsc/bindings/S3Error.cpp                      |  2 +-
 src/jsc/bindings/bindings.cpp                     |  2 +-
 test/js/node/errors/error-code-enumerable.test.ts | 93 +++++++++++++++++++++++
 5 files changed, 119 insertions(+), 18 deletions(-)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                               reads  edits  tests
src/jsc/bindings/BunProcess.cpp                        2      0     18
src/jsc/bindings/ErrorCode.cpp                         2      2     18
src/jsc/bindings/S3Error.cpp                           1      1     18
src/jsc/bindings/bindings.cpp                          1      1     18
test/js/node/errors/error-code-enumerable.test.ts      1     11     16

Node assigns code as a plain own property on ERR_* errors, on
SystemError and on process warning objects. Bun kept it on the cached
prototype or marked it DontEnum, so Object.keys, JSON.stringify and
object spread dropped it. The same applied to S3Error.
…ouched

Drop code from the shared per-code prototype so a mutated prototype
cannot change or remove the code of later instances, and so delete
err.code leaves it undefined as in Node. Only the Error that emitWarning
creates for a string warning gets name, code and detail, matching Node's
createWarningObject. An Error passed to emitWarning is emitted as is.
@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:56 PM PT - Sep 6th, 2026

⏳ @robobun, your commit 06af0af is still building in Build #111744, but has 3 failures so far (All Failures):

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced with the script from the report on Bun 1.4.3 (keys: [], json: "{}" for Buffer.alloc(-1), ERR_UNHANDLED_ERROR and a process.emitWarning() object) and compared against Node v26.3.0. test/js/node/errors/error-code-enumerable.test.ts fails 6 of 6 with USE_SYSTEM_BUN=1 and passes with the debug build.

CI (build 111744): the new test passes on every lane. The red jobs are tests this diff does not touch (test-crypto-dh-leak.js on x64-asan, which is red on main, test-process-finalization.mjs, the bun run Ctrl+C TTY test on alpine aarch64, a streams GC-count test in the darwin aarch64 parallel batch) plus two darwin x64 jobs that expired without an agent.

Comment thread src/jsc/bindings/BunProcess.cpp Resolved
Comment thread src/jsc/bindings/ErrorCode.cpp Resolved
Comment thread src/jsc/bindings/ErrorCode.cpp Resolved
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 368c6f5c-39ed-4603-b3ec-afd94247e355

📥 Commits

Reviewing files that changed from the base of the PR and between d316760 and 3e90537.

📒 Files selected for processing (5)
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/S3Error.cpp
  • src/jsc/bindings/bindings.cpp
  • test/js/node/errors/error-code-enumerable.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

Error and warning properties now use enumerable own-property semantics. Error codes are assigned per instance instead of shared prototypes. S3 and system error codes are enumerable. Tests cover descriptors, serialization, mutation, and warnings.

Error property enumerability

Layer / File(s) Summary
Own-property error construction
src/jsc/bindings/ErrorCode.cpp, src/jsc/bindings/S3Error.cpp, src/jsc/bindings/bindings.cpp, src/jsc/bindings/BunProcess.cpp
Error codes are assigned as enumerable own properties. String warnings assign enumerable name, code, and detail properties.
Compatibility validation
test/js/node/errors/error-code-enumerable.test.ts
Tests verify descriptors, property order, serialization, mutation, system-error metadata, and warning behavior.

Suggested reviewers: alii, dylan-conway

Merge Risk: ⚪ Minimal · up to 3e905

This change makes error and warning metadata serialize and enumerate like Node.js while preserving existing Error objects passed to emitWarning(). No current merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: making code an own enumerable property on Node errors and warnings.
Description check ✅ Passed The description clearly explains the problem, implementation, behavioral impact, and verification results. It does not use the exact template headings, but it provides all required information in equi…

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Trimmed the three flagged comments to one line each in 83a4fe6. No code change.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/jsc/bindings/webcore/streams/WebStreamsMisc.cpp — nit: this comment ("Bun's coded errors keep code on a per-code prototype") is now stale — after this change code is an own instance property and the prototype no longer carries it. The loop still works (it hits the own property first), but the rationale it documents is wrong. Fix: update the comment to say the walk covers both own code and any inherited data code without running getters.

    Extended reasoning...

    errorCodeIs walks error and its prototype chain calling getDirect(vm, codePublicName()) on each. Before this PR, ERR_* errors had code only on the cached per-code prototype, which is exactly what the comment describes. After this PR, putOwnCode (ErrorCode.cpp:148-152) puts code directly on the instance and createErrorPrototype no longer writes it to the prototype at all (ErrorCode.cpp:140-141). No functional break — getDirect on the instance returns the value on the first iteration — but the comment now misleads a future reader into thinking the prototype walk is load-bearing for Bun's own coded errors.

    Verification: nit — the parenthetical in the comment is made stale by this PR's change to ErrorCode.cpp; no functional break. WebStreamsMisc.cpp:431-432 (unchanged in this diff) reads: // 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. Before this PR, ErrorCode.cpp did put…

Comment thread src/jsc/bindings/ErrorCode.cpp Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Node's NodeError toString closes over the code, so it still prints it
after the own code property is reassigned or deleted. Stash the code on
the per-code toString function and read it from the callee. Also drop a
comment in WebStreamsMisc.cpp that described the old prototype layout.
@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the two review notes:

  • NodeError toString() no longer reads this.code. The code is stashed on the per-code toString function and read from the callee, which is what Node's closure over key amounts to. After err.code = "X" or delete err.code, String(err) still prints the original code, and err.code itself behaves like a plain property (dd20209).
  • Updated the stale errorCodeIs comment in WebStreamsMisc.cpp (dd20209, 06af0af). The lookup itself is unchanged: it now finds the own code on the first iteration.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants