Skip to content

refactor(codemode): give errors a real prototype chain and JS error types - #48559

Merged
rekram1-node merged 1 commit into
v2from
error-objects-pr
Sep 12, 2026
Merged

refactor(codemode): give errors a real prototype chain and JS error types#48559
rekram1-node merged 1 commit into
v2from
error-objects-pr

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Third step of the value-model rewrite (after #48527, #48541): error values get a real prototype chain, and failures the interpreter raises are named the way JavaScript names them.

The model

Error.prototype             { name: "Error", message: "", constructor: Error }
└── TypeError.prototype     { name: "TypeError", message: "", constructor: TypeError }
    └── new TypeError("m")  { message: "m" }                  ← own; name is inherited, as in JS
-ProgramError extends ProgramObject { errorName }   # brand string; instanceof compared names
+ProgramError extends ProgramObject {}              # the [[ErrorData]] slot; identity is the prototype chain
+hasPrototype(value, proto)                         # OrdinaryHasInstance step 3–4

The prototypes are intrinsics: a fixed, typed table built once per runtime, the way an engine's CreateIntrinsics does it.

// src/interpreter/intrinsics.ts
export type ErrorType = "Error" | "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" | "EvalError" | "URIError" | "AggregateError"
export type Intrinsics = { errors: Readonly<Record<ErrorType, ProgramObject>> }
export const createIntrinsics = (): Intrinsics => 
export const createErrorValue = (prototype: ProgramObject, message: string | undefined): ProgramError => 

Runner carries intrinsics. Each error constructor attaches itself as prototype.constructor when the globals are built (QuickJS wires %X.prototype%.constructor the same way, in JS_NewGlobalCConstructor). createErrorValue takes the prototype object, not a name, and installs an own message only when one was given (spec 20.5.1.1).

The host boundary serializes errors by brand, not by enumerable own properties (V8 WriteJSError does the same): return new Error("m") and JSON.stringify(new Error("m")) still give { name: "Error", message: "m" }.

Error types

Each interpreter throw site names the JS class the program catches, the way V8 picks NewTypeError vs NewRangeError. TypeError is the default because nearly every runtime failure is one; the four other classes have factories.

 class InterpreterRuntimeError {
-  errorName = "Error"          // mutable; overridden with a fluent .as("…")
+  readonly type: ErrorType = "TypeError"
 }
+export const rangeError = failure("RangeError")
+export const referenceError = failure("ReferenceError")
+export const syntaxError = failure("SyntaxError")
+export const uriError = failure("URIError")
program sees when
TypeError default: null.x, const { a } = undefined, x() on a non-function, tools + 1, atob("!")
RangeError rangeError(…): invalid array length, Array.with out of range, toString radix, repeat count, invalid Date
ReferenceError referenceError(…): unresolved binding, TDZ access
SyntaxError syntaxError(…): bad JSON.parse input, bad regexp flags; unsupported syntax reached at runtime
URIError uriError(…): malformed URI data
Error awaited tool failures, as before

The agent-facing diagnostic kind (InvalidDataValue, UnsupportedSyntax, …) is independent of the class. .as(…) is gone; Array.with and Number#toString radix were Error and are now correctly RangeError; atob/btoa no longer invent an InvalidCharacterError class (there is no DOMException).

What moved

src/interpreter/
├── intrinsics.ts   new: ErrorType, Intrinsics, createIntrinsics, createErrorValue
├── objects.ts      ProgramError is the [[ErrorData]] brand; +hasPrototype
├── model.ts        readonly type: ErrorType; rangeError/referenceError/syntaxError/uriError
├── runner.ts       +intrinsics on Runner
├── runtime.ts      createIntrinsics() per runtime; constructor fallback no longer special-cases errors
├── errors.ts       errorGlobal(type) wires prototype.constructor, instanceof via hasPrototype
├── globals.ts      error constructors from errorTypes
└── promises.ts     pass runner through
src/data.ts           errors serialize by brand: { name, message, ...own }
src/stdlib/value.ts   errorBrandName, errorConstructors, createErrorValue removed
src/stdlib/web.ts     atob/btoa throw TypeError
test/test262/run.ts   Test262Error is an ordinary prototype + constructor built from public pieces, not an Error
                      subclass (as in the real harness); assert.throws compares thrown.constructor

Behavior that changed

 try { const { a } = null } catch (e) {
-  e.name                       // "Error"
+  e.name                       // "TypeError"
-  e instanceof TypeError       // false
+  e instanceof TypeError       // true
 }
 const { constructor } = new RangeError("r")
-constructor === RangeError     // false — only e.constructor (member access) worked
+constructor === RangeError     // true
-Object.keys(new Error("m"))    // ["name", "message"]
+Object.keys(new Error("m"))    // ["message"]   (JS: [] — enumerability flags come later)
 return new Error("m")          // { name: "Error", message: "m" }   unchanged
 new RangeError("r") instanceof Error    // true (unchanged)

Tests: the JSON.parse(5)-is-a-plain-Error test now asserts the rule (null.x, tools + 1TypeError; class A {}SyntaxError); the WPT atob/btoa tests check for TypeError; the error-serialization tests assert inherited name / own message; one new test covers the chain.

Results

before after
existing suite + typecheck green green
test262, vendored Array/prototype + language/statements 486 skipped 248 skipped, 0 new failures
test262, wider local baseline (10 311 files) 1432 skipped 1119 skipped, 313 recovered, 0 new failures

Next

1  program-objects      #48527
2  function-objects     #48541
3  error-objects        ← this PR
4  prototype-methods    built-ins move onto real prototypes; drop the .prototype test262 boundary

@rekram1-node
rekram1-node force-pushed the error-objects-pr branch 4 times, most recently from 2a6e7b0 to b6bfdbb Compare September 12, 2026 02:10
@rekram1-node
rekram1-node merged commit b2e6e76 into v2 Sep 12, 2026
11 of 12 checks passed
@rekram1-node
rekram1-node deleted the error-objects-pr branch September 12, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant