Read an Error subclass's own code through NodeJS.ErrnoException (#1471)

This commit is contained in:
Chris Tate
2026-10-19 23:59:55 -05:00
committed by GitHub
4 changed files with 86 additions and 3 deletions
@@ -1,7 +1,16 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { BOOL, CAUGHT, DYN, F64, type IrExpr, type IrType, STRING } from "../../../ir/ir.js";
import {
BOOL,
CAUGHT,
DYN,
DYN_CLASS_PROPERTIES,
F64,
type IrExpr,
type IrType,
STRING,
} from "../../../ir/ir.js";
const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
"actual",
@@ -126,7 +135,13 @@ export function lowerErrorCodeProperty(
declared,
);
}
const systemField = ["errno", "syscall", "path", "dest"].includes(expr.name.text);
// A program class under the receiver's class can carry its own `code`
// (a declared field or a property added through an untyped reference),
// which Node reads instead of the errno code: read through the error's
// dynamic view, which answers both.
const systemField =
["errno", "syscall", "path", "dest"].includes(expr.name.text) ||
(expr.name.text === "code" && programClassMayOwnCode(lowerer, recvT.className));
if (
!systemField &&
expr.name.text !== "code" &&
@@ -162,7 +177,9 @@ export function lowerErrorCodeProperty(
type: DYN,
loc,
},
lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING),
expr.name.text === "code"
? lowerer.envValueType()
: lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING),
);
}
return {
@@ -184,6 +201,19 @@ export function lowerErrorCodeProperty(
};
}
/** Whether a class declared in the program, `className` or one of its
* subclasses, has its own `code` field or a dynamic property bag. The
* runtime error classes keep `code` in the errno slot. */
function programClassMayOwnCode(lowerer: Lowerer, className: string): boolean {
for (const [name, info] of lowerer.classes) {
if (name.startsWith("%")) continue;
if (name !== className && !lowerer.isSubclassOf(name, className)) continue;
if (info.def.fields.some((f) => f.name === "code" || f.name === DYN_CLASS_PROPERTIES))
return true;
}
return false;
}
/** Whether values of `type` are errors: class instances whose class is
* %Error or extends it. A subclass of a class that does not extend Error
* cannot be an error either, so the answer depends on the static class
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6348-errno-code-own-field.ts"
],
"diags": []
}
+46
View File
@@ -0,0 +1,46 @@
// `(err as NodeJS.ErrnoException).code` reads an Error subclass's own
// `code` field, as Node reads the own property, and still reads the errno
// code of runtime errors and undefined for plain errors.
import { readFileSync } from "node:fs";
class AppError extends Error {
code: string;
constructor(message: string, code = "E_APP") {
super(message);
this.name = "AppError";
this.code = code;
}
}
class DbError extends AppError {
constructor() {
super("db", "E_DB");
}
}
class Later extends Error {
code?: string;
}
function codeOf(err: unknown): string {
if (err instanceof Error) return (err as NodeJS.ErrnoException).code ?? "(none)";
return "(not an error)";
}
console.log(codeOf(new AppError("direct")));
console.log(codeOf(new DbError()));
const wrapped = new TypeError("wrapped", { cause: new AppError("cause", "E_CAUSE") });
console.log(codeOf(wrapped.cause), codeOf(wrapped));
try {
throw new DbError();
} catch (err) {
console.log(codeOf(err));
}
const later = new Later("later");
console.log(codeOf(later));
later.code = "E_LATER";
console.log(codeOf(later));
try {
readFileSync("/nonexistent/scriptc-corpus-6348");
} catch (err) {
console.log(codeOf(err));
}
console.log(codeOf(new Error("plain")), codeOf(new RangeError("range")), codeOf("text"));
@@ -0,0 +1 @@
{ "plain": 1200, "san": 1500 }