]> git.kaiwu.me - njs.git/commitdiff
Attach stack in error constructors.
authorDmitry Volyntsev <xeioex@nginx.com>
Fri, 30 Jan 2026 01:18:31 +0000 (17:18 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 5 Feb 2026 23:32:12 +0000 (15:32 -0800)
This aligns njs with QuickJS and most other JS engines.

12 files changed:
src/njs_error.c
src/njs_error.h
src/njs_promise.c
src/njs_vmcode.c
src/test/njs_unit_test.c
test/js/error_stack_created.t.js [new file with mode: 0644]
test/js/error_stack_promise_all.t.js [new file with mode: 0644]
test/js/error_stack_promise_any_inner.t.js [new file with mode: 0644]
test/js/error_stack_promise_race.t.js [new file with mode: 0644]
test/js/error_stack_promise_reject.t.js [new file with mode: 0644]
test/js/error_stack_type_error.t.js [new file with mode: 0644]
test/shell_test_njs.exp

index 13ed32aae51869a1808998762cbd52ecf71a0914..eaa8c65512333e7e0d8f8c9d7e653dc20aaf3868 100644 (file)
@@ -68,7 +68,7 @@ njs_error_fmt_new(njs_vm_t *vm, njs_value_t *dst, njs_object_type_t type,
 
 
 void
-njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
+njs_error_stack_attach(njs_vm_t *vm, njs_value_t value, njs_uint_t skip)
 {
     size_t               count;
     uint32_t             line, prev_line;
@@ -96,6 +96,11 @@ njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
     prev_name = njs_str_value("");
 
     for (frame = vm->top_frame; frame != NULL; frame = frame->previous) {
+        if (skip != 0) {
+            skip--;
+            continue;
+        }
+
         function = frame->native ? frame->function : NULL;
 
         if (function != NULL && function->bound != NULL) {
@@ -380,6 +385,8 @@ njs_error_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 
     njs_set_object(retval, error);
 
+    njs_error_stack_attach(vm, *retval, 1);
+
     return NJS_OK;
 }
 
index 4660660ba794fd3d0769b400162d30c9fcc24ba0..9c42b14ac36335ea664d7c9c1e17677ed4ec40db 100644 (file)
@@ -43,7 +43,7 @@ njs_object_t *njs_error_alloc(njs_vm_t *vm, njs_object_t *proto,
 njs_int_t njs_error_to_string(njs_vm_t *vm, njs_value_t *retval,
     const njs_value_t *error);
 njs_int_t njs_error_stack(njs_vm_t *vm, njs_value_t *value, njs_value_t *stack);
-void njs_error_stack_attach(njs_vm_t *vm, njs_value_t value);
+void njs_error_stack_attach(njs_vm_t *vm, njs_value_t value, njs_uint_t skip);
 
 
 extern const njs_object_type_init_t  njs_error_type_init;
index 53a8c4e25838f938d63f8d1087a2d7755642bc3c..016c6859766e402abf902bdbf8996cf4a8224385 100644 (file)
@@ -1287,6 +1287,8 @@ njs_promise_perform_all(njs_vm_t *vm, njs_value_t *iterator,
             }
 
             njs_set_object(&argument, error);
+
+            njs_error_stack_attach(vm, argument, 1);
         }
 
         ret = njs_function_call(vm, njs_function(&pargs->capability->resolve),
@@ -1664,6 +1666,8 @@ njs_promise_any_reject_element_functions(njs_vm_t *vm, njs_value_t *args,
 
         njs_set_object(&argument, error);
 
+        njs_error_stack_attach(vm, argument, 1);
+
         return njs_function_call(vm, njs_function(&context->capability->reject),
                                  &njs_value_undefined, &argument, 1,
                                  retval);
index 81d3bdd72a2e4f836c4a69960e78da7e883296bb..6d991042c7408b399388fbf5694b362cfa319fb4 100644 (file)
@@ -1854,7 +1854,7 @@ error:
     if (njs_is_error(&vm->exception)) {
         vm->active_frame->native.pc = pc;
 
-        njs_error_stack_attach(vm, vm->exception);
+        njs_error_stack_attach(vm, vm->exception, 0);
     }
 
     for ( ;; ) {
index ddb92c047dbfd1e3605996c923957e91e1a2efdf..215bea9e51c5012fb46eaad3f0f4f0d3532ad122 100644 (file)
@@ -21403,7 +21403,7 @@ static njs_unit_test_t  njs_shell_test[] =
       njs_str("9") },
 
     { njs_str("var e = Error(); e.name = {}; e" ENTER),
-      njs_str("[object Object]") },
+      njs_str("[object Object]\n    at main (:1)\n") },
 
     { njs_str("var a = []; Object.defineProperty(a, 'b', {enumerable: true, get: Object}); a" ENTER),
       njs_str("[\n b: '[Getter]'\n]") },
@@ -21411,12 +21411,12 @@ static njs_unit_test_t  njs_shell_test[] =
     { njs_str("var e = Error()" ENTER
               "Object.defineProperty(e, 'message', { configurable: true, set: Object })" ENTER
               "delete e.message; e" ENTER),
-      njs_str("Error") },
+      njs_str("Error\n    at main (:1)\n") },
 
     { njs_str("var e = Error()" ENTER
               "Object.defineProperty(e, 'message', { configurable: true, get(){ return 'foo'} })" ENTER
               "e" ENTER),
-      njs_str("Error: foo") },
+      njs_str("Error: foo\n    at main (:1)\n") },
 
     { njs_str("function f() {};" ENTER
               "Object.defineProperty(f, 'name', { get() {void(0)} })" ENTER
@@ -21676,6 +21676,13 @@ static njs_unit_test_t  njs_backtraces_test[] =
               "    at every (native)\n"
               "    at main (:1)\n") },
 
+    { njs_str("var p = new Promise((_, reject) => { reject(new Error('oops'));});"
+              "p.catch((e) => { $r.retval(e.stack) });"),
+      njs_str("Error: oops\n"
+              "    at <anonymous> (:1)\n"
+              "    at Promise (native)\n"
+              "    at main (:1)\n") },
+
     { njs_str("var e = new Error('oops'); e.stack = 123; e.stack"),
       njs_str("123") },
 
diff --git a/test/js/error_stack_created.t.js b/test/js/error_stack_created.t.js
new file mode 100644 (file)
index 0000000..4bfa3ad
--- /dev/null
@@ -0,0 +1,21 @@
+/*---
+includes: []
+flags: []
+---*/
+
+// Error.stack should be available when error is created (not just thrown)
+// This matches V8, SpiderMonkey, and QuickJS behavior
+
+function inner() {
+    return new Error("test error");
+}
+
+function outer() {
+    return inner();
+}
+
+var e = outer();
+
+assert.sameValue(typeof e.stack, 'string');
+assert.sameValue(e.stack.includes('inner'), true);
+assert.sameValue(e.stack.includes('outer'), true);
diff --git a/test/js/error_stack_promise_all.t.js b/test/js/error_stack_promise_all.t.js
new file mode 100644 (file)
index 0000000..147943c
--- /dev/null
@@ -0,0 +1,18 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+// Error.stack should be available in Promise.all rejection
+
+var p1 = new Promise((resolve, reject) => {
+    reject(new Error("promise failed"));
+});
+var p2 = Promise.resolve("ok");
+
+Promise.all([p1, p2])
+.then(v => $DONOTEVALUATE())
+.catch(e => {
+    assert.sameValue(typeof e.stack, 'string');
+})
+.then($DONE, $DONE);
diff --git a/test/js/error_stack_promise_any_inner.t.js b/test/js/error_stack_promise_any_inner.t.js
new file mode 100644 (file)
index 0000000..abbe5e0
--- /dev/null
@@ -0,0 +1,19 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+// Inner errors in AggregateError.errors should have stack traces
+
+var p1 = Promise.reject(new Error("inner error 1"));
+var p2 = Promise.reject(new Error("inner error 2"));
+
+Promise.any([p1, p2])
+.then(v => $DONOTEVALUATE())
+.catch(e => {
+    assert.sameValue(e.name, 'AggregateError');
+    assert.sameValue(e.errors.length, 2);
+    assert.sameValue(typeof e.errors[0].stack, 'string');
+    assert.sameValue(typeof e.errors[1].stack, 'string');
+})
+.then($DONE, $DONE);
diff --git a/test/js/error_stack_promise_race.t.js b/test/js/error_stack_promise_race.t.js
new file mode 100644 (file)
index 0000000..6b8594f
--- /dev/null
@@ -0,0 +1,17 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+// Error.stack should be available in Promise.race rejection
+
+var p1 = new Promise((resolve, reject) => {
+    reject(new Error("race failed"));
+});
+
+Promise.race([p1])
+.then(v => $DONOTEVALUATE())
+.catch(e => {
+    assert.sameValue(typeof e.stack, 'string');
+})
+.then($DONE, $DONE);
diff --git a/test/js/error_stack_promise_reject.t.js b/test/js/error_stack_promise_reject.t.js
new file mode 100644 (file)
index 0000000..f237fb7
--- /dev/null
@@ -0,0 +1,19 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+// Error.stack should be available when error is passed to reject()
+
+function rejectWithError() {
+    return new Promise((resolve, reject) => {
+        reject(new Error("rejected"));
+    });
+}
+
+rejectWithError()
+.catch(e => {
+    assert.sameValue(typeof e.stack, 'string');
+    assert.sameValue(e.stack.includes('Error'), true);
+})
+.then($DONE, $DONE);
diff --git a/test/js/error_stack_type_error.t.js b/test/js/error_stack_type_error.t.js
new file mode 100644 (file)
index 0000000..431b101
--- /dev/null
@@ -0,0 +1,16 @@
+/*---
+includes: []
+flags: []
+---*/
+
+// TypeError.stack should be available when error is created
+
+function createTypeError() {
+    return new TypeError("type error message");
+}
+
+var e = createTypeError();
+
+assert.sameValue(typeof e.stack, 'string');
+assert.sameValue(e.stack.includes('TypeError'), true);
+assert.sameValue(e.stack.includes('createTypeError'), true);
index 6dea0442ee8efe054f4a6d1487de35c545aea6b7..35b8233961f6d5c5eb9d0b4d8d162eabf3262f08 100644 (file)
@@ -90,7 +90,7 @@ njs_test {
 
 njs_test {
     {"[InternalError(),TypeError('msg'), new RegExp(), /^undef$/m, new Date(0)]\r\n"
-     "\\\[\r\n InternalError,\r\n TypeError: msg,\r\n /(?:)/,\r\n /^undef$/m,\r\n 1970-01-01T00:00:00.000Z\r\n]"}
+     "\\\[\r\n InternalError\r\n    at main \\(shell:1\\)\r\n,\r\n TypeError: msg\r\n    at main \\(shell:1\\)\r\n,\r\n /(?:)/,\r\n /^undef$/m,\r\n 1970-01-01T00:00:00.000Z\r\n]"}
 }
 
 # dumper excapes special characters as JSON.stringify()