Repository navigation
Conversation
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.
|
Updated 12:56 PM PT - Sep 6th, 2026
⏳ @robobun, your commit 06af0af is still building in
|
|
Status: ready for review. Reproduced with the script from the report on Bun 1.4.3 ( CI (build 111744): the new test passes on every lane. The red jobs are tests this diff does not touch ( |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (5)
Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review. WalkthroughChangesError 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
Suggested reviewers: Merge Risk: ⚪ Minimal · up to 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)
Comment |
|
Trimmed the three flagged comments to one line each in 83a4fe6. No code change. |
There was a problem hiding this comment.
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 keepcodeon a per-code prototype") is now stale — after this changecodeis 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 owncodeand any inherited datacodewithout running getters.Extended reasoning...
errorCodeIswalkserrorand its prototype chain callinggetDirect(vm, codePublicName())on each. Before this PR, ERR_* errors hadcodeonly on the cached per-code prototype, which is exactly what the comment describes. After this PR,putOwnCode(ErrorCode.cpp:148-152) putscodedirectly on the instance andcreateErrorPrototypeno longer writes it to the prototype at all (ErrorCode.cpp:140-141). No functional break —getDirecton 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…
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.
|
Addressed the two review notes:
|
Problem
codeon everyERR_*error is not an own enumerable property.Object.keys(err)is[],JSON.stringify(err)is{},{ ...err }.codeisundefined. Node shows{ code: 'ERR_OUT_OF_RANGE' }in each case.process.emitWarning()objects (name/code/detail),ERR_SYSTEM_ERRORandS3Errorhave the same gap.createErrorPrototype(src/jsc/bindings/ErrorCode.cpp:138) putcodeon the cached per-code prototype, never on the instance.for (k in err)also leakedname,code,toStringfrom there. The other three sites passedDontEnum.Fix
putOwnCodeputs the table's code on the instance as a plain own property at the threeErrorInstance::createsites inErrorCode.cpp. The shared prototype keepsnameandtoString, nowDontEnum. Sofor...inyieldscodealone anddelete err.codeleavesundefined, as in Node.Process::emitWarningassignsname/code/detailwithoutDontEnum, and only on the Error it creates for a string warning. An Error passed in is emitted untouched, like Node'screateWarningObjectpath.codeonERR_SYSTEM_ERRORand onS3ErrordropsDontEnum.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
ERR_*error, thrown from C++, Rust or built-in JS, goes throughErrorCodeCache::createError. It caches oneStructureper code. Its prototype sits between the instance andTypeError.prototype(orRangeError, ...) and carriesnameand a Node-styletoString(Name [CODE]: message). Node'slib/internal/errors.jshas the same shape and doeserror.code = keyon the instance.putDirect(vm, name, value, 0)creates the same writable, enumerable, configurable property as a plain JS assignment.PropertyAttribute::DontEnumhides it fromObject.keys,JSON.stringify, spread andfor...in.Notes
DontDeletefrom the errno-familySystemErrorproperties. This one leaves those alone to keep the diff to the enumerability question.emitWarning(anError, { code, detail })attached a hiddencode/detailto the caller's Error. Node does not touch it, so neither does Bun now.Object.getPrototypeOf(err)and delete or reassign properties on it, so reading from it is not safe.code: "ERR_..."from the instance.util.inspect(err)now appends{ code: 'ERR_...' }like Node.e.toString()still returnsRangeError [ERR_OUT_OF_RANGE]: ...through the prototypetoString. That function carries the code under a private name and reads it from the callee, like Node's closure overkey, so the output survivesdelete err.codeor a reassignederr.code.Process::emitWarningcallers (crypto deprecations,process.binding('uv'), MessagePort, timers, IPC, unhandled rejections) pass a string warning, so moving thecode/detailassignment into the string branch does not change them.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 vendoredtest-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.USERunset in this container, timing tests under ASAN).Before:
keys: [],json: "{}",inspectHasCode: falseon 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)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 1 rejected · iteration 1
evidence per changed file