Skip to content

13.5 broke tap's error handling #31142

Closed
Closed
@isaacs

Description

@isaacs
  • Version: 13.5.0
  • Platform: Darwin tau.local 19.0.0 Darwin Kernel Version 19.0.0: Thu Oct 17 16:17:15 PDT 2019; root:xnu-6153.41.3~29/RELEASE_X86_64 x86_64 (but irrelevant to this issue)
  • Subsystem: async hooks

The change in #30965 means that Node now relies on process._fatalException calling emitAfter(asyncId);

However, this cannot be done in userland code, because the emitAfter method is not exposed.

With the deprecation of domains in Node.js version 4, as far as I've been able to tell, patching process._fatalException and tracking the process flow through async hooks is the only way to reliably tie an error back to a chain of continuations. (And it doesn't even work 100% reliably, because Promises break async hooks.)

For node-tap, in order to fail the appropriate test when an error is thrown or promise is rejected (most of the time, at least), I'm using async-hook-domain. I used to use domains, but was scared off by the noisy warnings.

Now, a test like this:

const t = require('tap')
t.test('failure', t => {
  throw new Error('nope')
})

which should output this:

$ node fail.js
TAP version 13
# Subtest: failure
    not ok 1 - nope
      ---
      stack: |
        Test.<anonymous> (fail.js:3:9)
      at:
        line: 3
        column: 9
        file: fail.js
        function: Test.<anonymous>
      tapCaught: testFunctionThrow
      test: failure
      source: |
        t.test('failure', t => {
          throw new Error('nope')
        --------^
        })
      ...

    1..1
    # failed 1 test
not ok 1 - failure # time=18.611ms

instead (as of 503900b) outputs this:

$ node fail.js
TAP version 13
# Subtest: failure
    not ok 1 - nope
      ---
      stack: |
        Test.<anonymous> (fail.js:3:9)
      at:
        line: 3
        column: 9
        file: fail.js
        function: Test.<anonymous>
      tapCaught: testFunctionThrow
      test: failure
      source: |
        t.test('failure', t => {
          throw new Error('nope')
        --------^
        })
      ...

    1..1
    # failed 1 test
not ok 1 - failure # time=18.611ms

Error: async hook stack has become corrupted (actual: 18, expected: 1)
 1: 0x10000b8d9 node::AsyncHooks::pop_async_id(double) [/usr/local/bin/node]
 2: 0x10000189a node::InternalCallbackScope::Close() [/usr/local/bin/node]
 3: 0x10000149a node::InternalCallbackScope::~InternalCallbackScope() [/usr/local/bin/node]
 4: 0x1000b3f45 node::NodeMainInstance::Run() [/usr/local/bin/node]
 5: 0x10005e46f node::Start(int, char**) [/usr/local/bin/node]
 6: 0x7fff657932e5 start [/usr/lib/system/libdyld.dylib]
 7: 0x2

What has to happen to get error handling and continuation tracking shipped in core? The pieces are all there, they're just not exposed in any kind of useful way. I'm happy to help.

Failing that, I'd suggest reverting 503900b.

Metadata

Metadata

Assignees

No one assigned

    Labels

    async_hooksIssues and PRs related to the async hooks subsystem.confirmed-bugIssues with confirmed bugs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions