Skip to content

Async hook stack corruption with --unhandled-rejections=strict #60034

@sgravrock

Description

@sgravrock

Version

v24.3.0 (Other versions are also affected. See below.)

Platform

Darwin hood.local 24.6.0 Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000 arm64

Also reported on Windows 11.

Subsystem

No response

What steps will reproduce the bug?

First, install jasmine@5.11.0. Then create runner.js as follows:

const Jasmine = require("jasmine");
const fs = require("fs");

(async () => {
  const runner = new Jasmine();

  runner.addSpecFile("./crash.spec.js");

  runner.addReporter({
    suiteDone: async () => {
      console.log("\nAbout to crash!");
      await fs.promises.readFile("./package.json");
    },
  });

  await runner.execute();
})();

And create crash.spec.js as follows:

describe('a suite that crashes NodeJS', () => {
  it('throws twice', async () => {
        await new Promise((resolve, reject) => {
            setTimeout(() => {
                reject(new Error('foo'));
            }, 0);
        }).foo();
    });
});

Finally, run node --unhandled-rejections=strict runner.js.

I tried and failed to reduce this down to a simple code snippet with no dependencies. The user who reported it to me stated that it took them two days to reproduce it to this point.

How often does it reproduce? Is there a required condition?

Reproduces 100% of the time with (at least) 20.19.0, 22.14.0, 24.0.0, and 24.3.0. It's also been reported on >= 22.3.0. Does not reproduce with 20.0.0 or 22.0.0.

What is the expected behavior? Why is that the expected behavior?

Node should not crash. The script should run to completion and produce output like the following:

Randomized with seed 36538
Started
F
About to crash!


Failures:
1) a suite that crashes NodeJS throws twice
  Message:
    TypeError: (intermediate value).foo is not a function
  Stack:
        at UserContext.<anonymous> (/Users/steve/Downloads/no-zip/crash.spec.js:7:12)
        at <Jasmine>

Suite error: top suite
  Message:
    Uncaught exception: Error: foo
  Stack:
        at Timeout._onTimeout (/Users/steve/Downloads/no-zip/crash.spec.js:5:24)
        at listOnTimeout (node:internal/timers:573:17)
        at process.processTimers (node:internal/timers:514:7)
  Message:
    Unhandled promise rejection: Error: foo
  Stack:
        at Timeout._onTimeout (/Users/steve/Downloads/no-zip/crash.spec.js:5:24)
        at listOnTimeout (node:internal/timers:573:17)
        at process.processTimers (node:internal/timers:514:7)

1 spec, 3 failures
Finished in 0.007 seconds
Randomized with seed 36538 (jasmine --random=true --seed=36538)

What do you see instead?

Randomized with seed 66384
Started
F
About to crash!

  #  node[9176]: void node::AsyncHooks::push_async_context(double, double, Local<Object>) at ../src/env.cc:130
  #  Assertion failed: (trigger_async_id) >= (-1)

----- Native stack trace -----

 1: 0x10019f040 node::Assert(node::AssertionInfo const&) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 2: 0x102236be0 node::AsyncHooks::push_async_context(double, double, v8::Local<v8::Object>) (.cold.2) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 3: 0x1000dc85c node::AsyncHooks::grow_async_ids_stack() [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 4: 0x100098cb4 node::InternalCallbackScope::InternalCallbackScope(node::Environment*, v8::Local<v8::Object>, node::async_context const&, int, v8::Local<v8::Value>) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 5: 0x100098da0 node::InternalCallbackScope::InternalCallbackScope(node::AsyncWrap*, int) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 6: 0x100196cd4 node::fs::FSReqPromise<node::AliasedBufferBase<double, v8::Float64Array>>::Resolve(v8::Local<v8::Value>) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 7: 0x1001a5850 node::fs::AfterStat(uv_fs_s*) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 8: 0x1001973a8 node::MakeLibuvRequestCallback<uv_fs_s, void (*)(uv_fs_s*)>::Wrapper(uv_fs_s*) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
 9: 0x100fd12c4 uv__work_done [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
10: 0x100fd4ff4 uv__async_io [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
11: 0x100fe938c uv__io_poll [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
12: 0x100fd555c uv_run [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
13: 0x100099a98 node::SpinEventLoopInternal(node::Environment*) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
14: 0x1001eb704 node::NodeMainInstance::Run() [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
15: 0x100159474 node::Start(int, char**) [/Users/steve/.nvm/versions/node/v24.3.0/bin/node]
16: 0x19fc1ab98 start [/usr/lib/dyld]
zsh: abort      node --unhandled-rejections=strict runner.js

Additional information

If I replace await fs.promises.readFile("./package.json"); with await new Promise(resolve => setTimeout(resolve)); in runner.js, I sometimes get an early exit instead of a crash. It's easiest to see that if you patch node_modules/jasmine/lib/jasmine.js as follows:

@@ -239,7 +239,7 @@
       this.addMatchingSpecFiles(files);
     }
 
-    const prematureExitHandler = new ExitHandler(() => this.exit(4));
+    const prematureExitHandler = new ExitHandler(() => console.error('premature exit!'));
     prematureExitHandler.install();
     let overallResult;

Metadata

Metadata

Assignees

No one assigned

    Labels

    async_hooksIssues and PRs related to the async hooks subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions