Capture stack traces with Error.captureStackTrace as Node does

This commit is contained in:
Chris Tate
2026-10-22 06:52:18 -05:00
parent 41245eeb0c
commit c2ec0b989d
20 changed files with 530 additions and 110 deletions
+1
View File
@@ -7586,4 +7586,5 @@ declare module "node:stream/web" {
}
interface ErrorConstructor {
stackTraceLimit: number;
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
}
@@ -677,6 +677,32 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
if (e.fn === "error.captureStackTrace") {
// The second argument names the function constructorOpt skips through;
// its frame text is the constant its own frame pushes (emit-function.ts),
// or null when it has no frame, which leaves no frames.
const target = host.emitExpr(e.args[0]!);
const named = e.args[1]!;
const skip = e.args[2]!.kind === "boolLit" && e.args[2]!.value;
let through = "null";
if (named.kind === "strLit" && named.value !== "") {
const fn = host.mod.functions.find((f) => f.name === named.value);
if (
fn !== undefined &&
host.stackTraces &&
!host.wasi &&
fn.sourceName !== undefined &&
!host.unframedFunctions.has(fn.name)
)
through = host.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
}
host.declare(`declare void @scr_error_capture_stack_trace(ptr, ptr, i1)`);
B.line(
`call void @scr_error_capture_stack_trace(ptr ${target.name}, ptr ${through}, i1 ${skip ? "true" : "false"})`,
);
host.emitPendingCheck();
return { name: "", type: e.type };
}
if (e.fn === "error.argTypeThrow") {
// Always throws with the runtime-rendered Received tail (the
// error.nodeThrow dummy pattern). Borrows all three.
@@ -1506,7 +1506,6 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.objectPrototype": "scr_dyn_object_prototype",
"dyn.errorPrototype": "scr_dyn_error_prototype",
"dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype",
"dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace",
"dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function",
"dyn.functionApply": "scr_dyn_function_apply",
"dyn.builtinMethod": "scr_dyn_builtin_method",
@@ -1,7 +1,6 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import {
BOOL,
@@ -13,6 +12,7 @@ import {
type IrStmt,
type IrType,
STRING,
VOID,
} from "../../../ir/ir.js";
const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
@@ -324,60 +324,6 @@ export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression
: error;
}
/** `Error.captureStackTrace(target[, constructorOpt])` (also called as
* `Error.captureStackTrace?.(...)`, which always calls it). On a
* checked-dynamic target (`this` in a JavaScript constructor function) the
* runtime defines target's `stack`. An instance of a class that extends
* Error derives its stack from its name and message when the stack is
* read, so the call only evaluates its arguments. Both arguments are
* evaluated; the second only selects frames in Node. Null for other
* targets and callees. */
export function lowerErrorCaptureStackTraceCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.questionDotToken) return null;
if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null;
const args = call.arguments.nodes;
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
const probed = tryLowerExpression(lowerer, args[0]!);
const loc = locOf(call);
if (
probed?.type.kind === "object" &&
(probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error"))
) {
const stmts: IrStmt[] = [];
for (const arg of args) {
const value = lowerer.lowerExpr(arg);
if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc });
}
const result = dynUndefinedExpr(loc);
return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
}
if (probed?.type.kind !== "dyn") return null;
const target = lowerer.lowerExpr(args[0]!);
const result: IrExpr = {
kind: "libCall",
fn: "dyn.errorCaptureStackTrace",
args: [target],
type: DYN,
loc,
};
if (args.length === 1) return result;
const local = lowerer.declareHiddenLocal("%captureTarget", DYN);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: target, loc },
{ kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc },
],
result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] },
type: DYN,
loc,
};
}
/** `Error.isError(value)`. A catch binding tests its payload against the
* Error class, an `unknown` value tests the checked-dynamic tree's error
* encoding, and a union tests its runtime tag against the arms whose class
@@ -470,3 +416,120 @@ export function lowerErrorIsErrorCall(
loc,
};
}
/** The class whose constructor body `node` is directly in (not inside a
* nested function): while it runs, `new` of that class is on the stack. */
function enclosingConstructorClass(node: ts.Node): ts.ClassLikeDeclaration | undefined {
for (let n = node.parent; n !== undefined; n = n.parent) {
if (ts.isConstructorDeclaration(n))
return ts.isClassDeclaration(n.parent) || ts.isClassExpression(n.parent)
? n.parent
: undefined;
if (ts.isFunctionLike(n) && !ts.isArrowFunction(n)) return undefined;
}
return undefined;
}
/** `Error.captureStackTrace(target, constructorOpt?)`, also through `?.`:
* target.stack becomes the target's heading and the current stack frames,
* as V8 does it (scr_error_capture_stack_trace). The target is an error, a
* checked-dynamic object, or a primitive (a TypeError at run time).
* constructorOpt leaves out the frames up to and including its topmost call:
* a function declaration skips through its own frame; `this.constructor`,
* `new.target` or the class's own name inside a class constructor leave the
* frames from the code that ran `new`, which are the current frames
* (constructors have no frames of their own). Other targets are refused,
* and so are other constructorOpt values in TypeScript; JavaScript keeps all
* frames for them. Null when the callee is not the
* standard library's Error.captureStackTrace. */
export function lowerErrorCaptureStackTraceCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null;
const args = call.arguments.nodes;
const loc = locOf(call);
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
"Error.captureStackTrace arguments",
call,
"pass a target and an optional constructor",
);
}
const target = lowerer.lowerExpr(args[0]!);
// Primitives (and unions of them) reach the runtime, which throws V8's
// TypeError for them; objects without a checked-dynamic view are refused,
// since a boxed copy would not show the stack.
const primitive =
["f64", "string", "bool", "nullT", "undefinedT", "bigint"].includes(target.type.kind) ||
(target.type.kind === "union" &&
lowerer.dynConvertible(target.type) &&
(lowerer.unions.get(target.type.unionId)?.arms ?? []).every((arm) =>
["f64", "string", "bool", "nullT", "undefinedT", "bigint"].includes(arm.kind),
));
if (target.type.kind !== "dyn" && !primitive && !isErrorClassType(lowerer, target.type)) {
lowerer.noLowering(
`Error.captureStackTrace on '${lowerer.fmt(target.type)}' values`,
args[0]!,
"pass an Error instance, or an untyped object in JavaScript",
);
}
let through = "";
let skip = false;
const ctor = args[1];
if (ctor !== undefined) {
let node: ts.Expression = ctor;
while (ts.isParenthesizedExpression(node)) node = node.expression;
const symbol = ts.isIdentifier(node) ? lowerer.checker.getSymbolAtLocation(node) : undefined;
const resolved =
symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias
? lowerer.checker.getAliasedSymbol(symbol)
: symbol;
const cls = enclosingConstructorClass(call);
const constructing =
(ts.isPropertyAccessExpression(node) &&
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
node.name.text === "constructor") ||
(ts.isMetaProperty(node) && node.name.text === "target") ||
(resolved !== undefined &&
cls?.name !== undefined &&
lowerer.checker.getSymbolAtLocation(cls.name) === resolved);
const sig = resolved !== undefined ? lowerer.fnSigsBySymbol.get(resolved) : undefined;
if (constructing && cls !== undefined) {
skip = false;
} else if (sig !== undefined) {
lowerer.noteEdge(sig.name);
through = sig.name;
skip = true;
} else if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
// JavaScript (undici's `captureStackTrace(err, module.exports.fetch)`)
// keeps every frame: the same frames V8 keeps, and the ones above them.
skip = false;
} else {
lowerer.noLowering(
"Error.captureStackTrace with this constructor argument",
ctor,
"pass a function declaration, or this.constructor or the class inside its constructor",
);
}
}
const capture: IrExpr = {
kind: "libCall",
fn: "error.captureStackTrace",
args: [
lowerer.coerceToExpected(target, DYN),
{ kind: "strLit", value: through, type: STRING, loc },
{ kind: "boolLit", value: skip, type: BOOL, loc },
],
type: VOID,
loc,
};
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: capture, loc }],
result: dynUndefinedExpr(loc),
type: DYN,
loc,
};
}
@@ -418,6 +418,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const timeoutOptional = lowerer.lowerTimeoutMethodCall(expr, expr.expression);
if (timeoutOptional) return timeoutOptional;
}
// `Error.captureStackTrace?.(this, this.constructor)`: Node always has it.
if (
ts.isPropertyAccessExpression(expr.expression) &&
(expr.questionDotToken || expr.expression.questionDotToken)
) {
const captured = lowerErrorCaptureStackTraceCall(lowerer, expr, expr.expression);
if (captured) return captured;
}
// `req.session?.m(...)` remains absent on compatibility requests. Live
// `req.stream` calls now pass through the normal optional-chain and h2
// stream lowering using lowerServerProperty's checked getter.
+1 -3
View File
@@ -443,6 +443,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"js.toNumberCoerce",
// Error messages use the same coercion protocol before installing cause.
"error.newOptions",
"error.captureStackTrace",
"error.ctorOptions",
"error.newAggregate",
"error.newSuppressed",
@@ -610,9 +611,6 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
// Error.captureStackTrace: Node's TypeError for a primitive target, and
// the error string conversion of name and message
"dyn.errorCaptureStackTrace",
// a proxy in the prototype chain can throw
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
@@ -3369,6 +3369,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* Options retain cause presence; constructor calls borrow their args.
* Message coercion may throw before the cause is installed. */
"error.newOptions": { argTypes: [DYN, DYN], result: VOID },
"error.captureStackTrace": { argTypes: [DYN, STRING, BOOL], result: VOID },
"error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID },
/** Borrowed Error receiver. cause returns an owned dyn value (undefined
* when absent); hasCause distinguishes absence from present undefined. */
@@ -3682,7 +3683,6 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.objectPrototype": { argTypes: [], result: DYN },
"dyn.errorPrototype": { argTypes: [], result: DYN },
"dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL },
"dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN },
"dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN },
"dyn.functionApply": { argTypes: [], result: DYN },
"dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN },
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6873-error-capture-stack-trace.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6874-error-capture-stack-trace-objects.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/error-capture-stack-trace-fences.ts"
],
"diags": []
}
+1 -1
View File
@@ -1092,7 +1092,6 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt
ScrDyn *scr_dyn_object_prototype(void);
ScrDyn *scr_dyn_error_prototype(void);
bool scr_dyn_inherits_error_prototype(const ScrDyn *value);
ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target);
ScrDyn *scr_dyn_error_capture_stack_trace_function(void);
ScrDyn *scr_dyn_function_apply(void);
ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list);
@@ -1195,6 +1194,7 @@ ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */
/* The cache's runtime-internal access pair (scr_json.c owns the storage;
* the gated extraction reads/writes through these). */
ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */
void scr_error_capture_stack_trace(ScrDyn *target, const char *through, bool skip);
void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */
/* Sets an own property on an error: its checked-dynamic node, which casts
* and util.inspect read. Moves value. */
+20 -4
View File
@@ -196,19 +196,21 @@ void scr_stack_limit_set(double limit) {
scr_stack_limit = limit;
}
ScrStr *scr_stack_capture(void) {
if (!SCR_EXC_CUR()->stack) return NULL;
/* The frames from `first` down, at most Error.stackTraceLimit of them, one
* "\n at ..." line each; NULL when there are none. */
static ScrStr *scr_stack_capture_from(ScrStackFrame *first) {
if (!first) return NULL;
const double limit = floor(scr_stack_limit);
if (!(limit > 0)) return NULL;
size_t length = 0;
size_t count = 0;
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame && (double)count++ < limit; frame = frame->previous)
for (ScrStackFrame *frame = first; frame && (double)count++ < limit; frame = frame->previous)
length += 1 + strlen(frame->text);
char *text = malloc(length);
if (!text) scr_trap("scriptc: out of memory\n");
size_t offset = 0;
count = 0;
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame && (double)count++ < limit; frame = frame->previous) {
for (ScrStackFrame *frame = first; frame && (double)count++ < limit; frame = frame->previous) {
text[offset++] = '\n';
size_t size = strlen(frame->text);
memcpy(text + offset, frame->text, size);
@@ -219,6 +221,20 @@ ScrStr *scr_stack_capture(void) {
return out;
}
ScrStr *scr_stack_capture(void) {
return scr_stack_capture_from(SCR_EXC_CUR()->stack);
}
/* The frames below the topmost frame whose text is `through` (a function's
* " at name (file)" line), as Error.captureStackTrace's constructorOpt
* leaves them; NULL when no such frame is on the stack. */
ScrStr *scr_stack_capture_after(const char *through) {
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame; frame = frame->previous)
if (frame->text == through || strcmp(frame->text, through) == 0)
return scr_stack_capture_from(frame->previous);
return NULL;
}
#define scr_exc_kind (SCR_EXC_CUR()->kind)
#define scr_exc_f64 (SCR_EXC_CUR()->f64)
#define scr_exc_bool (SCR_EXC_CUR()->b)
+1
View File
@@ -68,6 +68,7 @@ typedef struct ScrStackFrame {
void scr_stack_enter(ScrStackFrame *frame, const char *text);
void scr_stack_leave(ScrStackFrame *frame);
ScrStr *scr_stack_capture(void); /* +1, NULL when no source frames exist */
ScrStr *scr_stack_capture_after(const char *through); /* +1, NULL when none remain */
double scr_stack_limit_get(void);
void scr_stack_limit_set(double limit);
+106 -44
View File
@@ -1950,52 +1950,13 @@ ScrDyn *scr_dyn_error_prototype(void) {
return scr_dyn_retain(scr_builtin_error_prototype);
}
/* Error.captureStackTrace(target): defines a non-enumerable own `stack`
* on target. Node's value is the Error.prototype.toString text of target
* (read without calling an overriding toString) followed by stack frames;
* here, as for scriptc's own Error instances, it is the text alone.
* Answers undefined, or NULL with the exception pending. */
ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target) {
/* An instance of a class that extends Error derives its stack from its
* name and message when the stack is read. */
if (target->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(target);
bool error = view && view->kind == SCR_DYN_OBJ && scr_dyn_obj_get(view, "%error", 6) != NULL;
scr_dyn_release(view);
if (scr_exc_pending()) return NULL;
if (error) return scr_dyn_retain(scr_dyn_undefined());
}
if (target->kind != SCR_DYN_OBJ) {
if (target->kind == SCR_DYN_ARR || target->kind == SCR_DYN_FUNC || target->kind == SCR_DYN_TYPED_REF ||
target->kind == SCR_DYN_HANDLE || target->kind == SCR_DYN_PROXY) {
static const char message[] = "Error.captureStackTrace on this object is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
} else {
static const char message[] = "invalid_argument";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
}
return NULL;
}
ScrDynEntry *existing = scr_dyn_find_entry(target, "stack", 5);
if (existing ? existing->accessor || !existing->configurable : target->non_extensible) {
static const char message[] = "Cannot redefine property: stack";
static const char extensible[] = "Cannot define property stack, object is not extensible";
if (existing) scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
else scr_throw_error_msg(SCR_ERR_TYPE, extensible, sizeof extensible - 1);
return NULL;
}
ScrStr *text = scr_dyn_error_string(target);
if (!text) return NULL;
scr_dyn_obj_set_field(target, "stack", 5, scr_dyn_new_str(text));
scr_str_release(text);
ScrDynEntry *entry = scr_dyn_find_entry(target, "stack", 5);
entry->enumerable = false;
entry->writable = entry->configurable = true;
return scr_dyn_retain(scr_dyn_undefined());
}
/* Error.captureStackTrace called through its function value. The
* constructor argument's frame cannot be found from a function value, so
* every frame is kept: the frames V8 keeps and the ones above them. */
static ScrDyn *scr_error_capture_stack_trace_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
return scr_dyn_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined());
scr_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined(), NULL, false);
return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined());
}
/* Error.captureStackTrace read as a value: one function per thread. */
static SCR_TL ScrDyn *scr_error_capture_stack_trace_fn;
@@ -15430,3 +15391,104 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) {
return true;
return scr_dyn_object_proto_name(key);
}
/* The `stack` accessor Error.captureStackTrace gives an object that is not
* an error. caps[0] holds the captured frames until the first read, then the
* stack; caps[1] is nonzero once caps[0] holds the stack. The first read
* formats it with the receiver's name and message then, as V8 does. */
static ScrDyn *scr_captured_stack_get_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)args; (void)argc;
ScrDyn *value = scr_box_get_ref(closure->caps[0]);
if (scr_box_get_f64(closure->caps[1]) != 0) return value;
ScrDyn *self = scr_dyn_this_get();
ScrStr *heading = scr_dyn_error_string(self);
scr_dyn_release(self);
if (!heading) {
scr_dyn_release(value);
return NULL;
}
ScrStr *stack = scr_str_concat(heading, value->v.str);
scr_str_release(heading);
scr_dyn_release(value);
ScrDyn *out = scr_dyn_new_str(stack);
scr_str_release(stack);
scr_box_set_ref(closure->caps[0], scr_dyn_retain(out));
scr_box_set_f64(closure->caps[1], 1);
return out;
}
static ScrDyn *scr_captured_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
scr_box_set_ref(closure->caps[0], scr_dyn_retain(argc > 0 ? args[0] : scr_dyn_undefined()));
scr_box_set_f64(closure->caps[1], 1);
return scr_dyn_retain(scr_dyn_undefined());
}
/* Error.captureStackTrace(target, constructorOpt), as V8 does it: target.stack
* becomes the target's heading and the current stack frames, formatted when
* first read. With `skip`, the frames up to and including the topmost one
* whose text is `through` are left out (none remain when `through` is NULL or
* not on the stack). An error keeps the frames as its own; another object
* gets an own non-enumerable, configurable `stack` accessor. A primitive
* target throws `TypeError: invalid_argument` and a non-extensible object
* V8's define-property TypeError. */
void scr_error_capture_stack_trace(ScrDyn *target, const char *through, bool skip) {
switch (target->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL:
case SCR_DYN_BOOL:
case SCR_DYN_NUM:
case SCR_DYN_STR:
case SCR_DYN_BIGINT:
case SCR_DYN_SYMBOL: {
static const char msg[] = "invalid_argument";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return;
}
default:
break;
}
ScrStr *frames = !skip ? scr_stack_capture() : through ? scr_stack_capture_after(through) : NULL;
ScrError *error = scr_errdyn_err_of(target);
if (error) {
scr_str_release(error->stack_frames);
error->stack_frames = frames;
scr_str_release(error->stack);
error->stack = NULL;
scr_error_release(error);
return;
}
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_ARR) {
static const char msg[] = "Error.captureStackTrace on this object is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020");
scr_str_release(frames);
return;
}
if (!scr_dyn_is_extensible(target)) {
static const char msg[] = "Cannot define property stack, object is not extensible";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
scr_str_release(frames);
return;
}
if (!frames) frames = scr_str_new("", 0);
/* The getter and setter share their boxes. */
ScrClosure *state[2] = {scr_closure_new(NULL, 2), scr_closure_new(NULL, 2)};
state[0]->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
state[0]->caps[1] = scr_box_new(SCR_BOX_F64);
scr_box_set_ref(state[0]->caps[0], scr_dyn_new_str(frames));
scr_str_release(frames);
for (int i = 0; i < 2; i++) state[1]->caps[i] = scr_box_retain(state[0]->caps[i]);
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "get", 3,
scr_dyn_new_func(state[0], scr_captured_stack_get_call, 0, "native:error.stack", NULL));
scr_dyn_obj_set(descriptor, "set", 3,
scr_dyn_new_func(state[1], scr_captured_stack_set_call, 1, "native:error.stack", NULL));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false));
scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true));
ScrStr *key_text = scr_str_new("stack", 5);
ScrDyn *key = scr_dyn_new_str(key_text);
scr_str_release(key_text);
ScrDyn *result = scr_dyn_define_property(target, key, descriptor);
scr_dyn_release(result);
scr_dyn_release(key);
scr_dyn_release(descriptor);
}
@@ -0,0 +1,87 @@
// Error.captureStackTrace on errors, as Node does it: the error's stack
// becomes its heading and the current frames, starting at the function that
// called it; a constructor argument leaves out the frames up to and
// including that function's topmost call, and every frame when it is not on
// the stack. Error subclasses that call it with this.constructor (also
// through ?.) keep the frames from the code that ran `new`.
function frames(stack: string | undefined): string {
const lines = (stack ?? "").split("\n");
const wanted = ["inner", "middle", "outer", "makeExpired", "skipped", "caller"];
const names = lines.slice(1).map((line) => line.trim().split(" ")[1] ?? "");
return `${lines[0]} | ${names.filter((name) => wanted.includes(name)).join(",")}`;
}
function inner(target: Error): Error {
Error.captureStackTrace(target);
return target;
}
function middle(target: Error): Error {
return inner(target);
}
function outer(target: Error): Error {
return middle(target);
}
console.log("error", frames(outer(new Error("plain")).stack));
console.log("type error", frames(outer(new TypeError("typed")).stack));
function skipped(target: Error): Error {
Error.captureStackTrace(target, skipped);
return target;
}
function caller(target: Error): Error {
return skipped(target);
}
console.log("constructorOpt", frames(caller(new Error("skip")).stack));
function unrelated(): void {}
const missing = new Error("missing");
Error.captureStackTrace(missing, unrelated);
console.log("not on the stack", JSON.stringify(missing.stack));
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = "AppError";
Error.captureStackTrace(this, this.constructor);
}
}
class Expired extends AppError {
constructor(message: string) {
super(message);
this.name = "Expired";
}
}
class Optional extends Error {
constructor(message: string) {
super(message);
this.name = "Optional";
Error.captureStackTrace?.(this, this.constructor);
}
}
class Named extends Error {
constructor(message: string) {
super(message);
Error.captureStackTrace(this, Named);
}
}
function makeExpired(): Error {
return new Expired("token expired");
}
const expired = makeExpired();
console.log("subclass", frames(expired.stack), expired instanceof AppError, expired.message);
console.log("optional call", frames(new Optional("maybe").stack));
console.log("class name", frames(new Named("named").stack));
const renamed = new RangeError("range");
renamed.name = "Custom";
Error.captureStackTrace(renamed);
console.log("renamed", frames(renamed.stack));
const late = new Error("late");
Error.captureStackTrace(late);
late.name = "Late";
console.log("renamed after", frames(late.stack));
Error.stackTraceLimit = 1;
console.log("limit", frames(outer(new Error("limited")).stack));
Error.stackTraceLimit = 0;
console.log("limit 0", JSON.stringify(outer(new Error("none")).stack));
Error.stackTraceLimit = 10;
@@ -0,0 +1,92 @@
// Error.captureStackTrace on objects in JavaScript, as Node does it: an own,
// non-enumerable, configurable `stack` accessor giving the object's heading
// (its name and message when first read, through Error.prototype.toString's
// rules, "Error" by default) and the frames from the caller; TypeErrors for a
// primitive and a non-extensible target.
'use strict';
function frames(stack) {
const lines = String(stack).split('\n');
const names = lines.slice(1).map((line) => line.trim().split(' ')[1]);
return `${lines[0]} | ${names.filter((name) => ['inner', 'middle', 'outer', 'skipped', 'caller'].includes(name)).join(',')}`;
}
function inner(target) {
Error.captureStackTrace(target);
return target;
}
function middle(target) {
return inner(target);
}
function outer(target) {
return middle(target);
}
console.log('plain', frames(outer({}).stack));
console.log('named', frames(outer({ name: 'Parse', message: 'bad token' }).stack));
console.log('message only', frames(outer({ message: 'just this' }).stack));
console.log('empty name', frames(outer({ name: '', message: 'no name' }).stack));
console.log('number fields', frames(outer({ name: 7, message: 8 }).stack));
const own = inner({ kept: 1 });
console.log('keys', JSON.stringify(Object.keys(own)), typeof own.stack);
const descriptor = Object.getOwnPropertyDescriptor(own, 'stack');
console.log('descriptor', typeof descriptor.get, typeof descriptor.set, descriptor.enumerable, descriptor.configurable);
own.stack = 'replaced';
console.log('writable', own.stack);
const later = inner({ message: 'first' });
later.name = 'Renamed';
later.message = 'second';
console.log('formatted when read', frames(later.stack));
later.name = 'Again';
console.log('formatted once', frames(later.stack));
const counted = { message: 'm' };
let reads = 0;
Object.defineProperty(counted, 'name', { get() { reads++; return 'Counted'; } });
inner(counted);
console.log('reads before stack', reads);
console.log('counted', frames(counted.stack), reads);
function skipped(target) {
Error.captureStackTrace(target, skipped);
return target;
}
function caller(target) {
return skipped(target);
}
console.log('constructorOpt', frames(caller({}).stack));
const arr = [];
Error.captureStackTrace(arr);
console.log('array', frames(arr.stack), arr.length);
for (const value of [5, 'text', undefined, null, true]) {
try {
Error.captureStackTrace(value);
console.log('primitive', String(value), 'no error');
} catch (error) {
console.log('primitive', String(value), error.name, error.message);
}
}
try {
Error.captureStackTrace(Object.freeze({}));
console.log('frozen', 'no error');
} catch (error) {
console.log('frozen', error.name, error.message);
}
const api = {
run(target) {
Error.captureStackTrace(target, api.run);
return target;
},
};
function viaApi(target) {
return api.run(target);
}
console.log('member constructorOpt', frames(viaApi({ message: 'member' }).stack).split(' | ')[0]);
const captureFn = Error.captureStackTrace;
function viaValue(target) {
captureFn(target);
return target;
}
const fromValue = viaValue({ message: 'value' });
fromValue.message = 'read later';
console.log('function value', frames(fromValue.stack), captureFn.name, captureFn.length);
try {
captureFn(7);
} catch (error) {
console.log('function value primitive', error.name, error.message);
}
@@ -0,0 +1,21 @@
// Error.captureStackTrace gives a stack to errors and untyped objects; a
// typed object has no stack property to receive it, and a constructor
// argument other than a function declaration or the constructing class
// cannot be found on the stack.
interface Report {
message: string;
}
const report: Report = { message: "typed" };
Error.captureStackTrace(report);
const arrow = (): void => {};
Error.captureStackTrace(new Error("arrow"), arrow);
class Other {}
class Wrapped extends Error {
constructor() {
super("wrapped");
Error.captureStackTrace(this, Other);
}
}
console.log(new Wrapped().stack);
@@ -0,0 +1,26 @@
error-capture-stack-trace-fences.ts:9:25 - error SC2020: 'Error.captureStackTrace on '{ message: string }' values' is part of the standard library types but has no scriptc lowering yet
8 | const report: Report = { message: "typed" };
9 | Error.captureStackTrace(report);
| ^~~~~~
10 |
hint: pass an Error instance, or an untyped object in JavaScript
error-capture-stack-trace-fences.ts:12:45 - error SC2020: 'Error.captureStackTrace with this constructor argument' is part of the standard library types but has no scriptc lowering yet
11 | const arrow = (): void => {};
12 | Error.captureStackTrace(new Error("arrow"), arrow);
| ^~~~~
13 |
hint: pass a function declaration, or this.constructor or the class inside its constructor
error-capture-stack-trace-fences.ts:18:35 - error SC2020: 'Error.captureStackTrace with this constructor argument' is part of the standard library types but has no scriptc lowering yet
17 | super("wrapped");
18 | Error.captureStackTrace(this, Other);
| ^~~~~
19 | }
hint: pass a function declaration, or this.constructor or the class inside its constructor
@@ -0,0 +1 @@
{ "plain": 2446, "san": 2267 }
@@ -0,0 +1 @@
{ "plain": 2286, "san": 3360 }