Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The unhandledRejection handler is not testable anymore. #5620

Open
despairblue opened this issue Feb 20, 2018 · 28 comments
Open

The unhandledRejection handler is not testable anymore. #5620

despairblue opened this issue Feb 20, 2018 · 28 comments

Comments

@despairblue
Copy link

Do you want to request a feature or report a bug?

Regression

What is the current behavior?

Current versions of jest don't allow testing unhandledRejections. The following test will never succeed:

const foo = require(".");

describe("foo", () => {
  test("bar", done => {
    process.removeAllListeners("unhandledRejection");
    process.on("unhandledRejection", function(error) {
      process.removeAllListeners("unhandledRejection");
      expect(error).toBeInstanceOf(Error);
      done();
    });

    foo();
  });
});

If the current behavior is a bug, please provide the steps to reproduce and
either a repl.it demo through https://repl.it/languages/jest or a minimal
repository on GitHub that we can yarn install and yarn test.

I made a repository with two commits with different jest versions showing that it works with an older version but not anymore with the current one. The README describes the repro steps in detail.

What is the expected behavior?

It should be possible to test unhandledRejection and uncaughtExceptions.

Btw I'm well aware that this is a case of https://xkcd.com/1172/ and that we could refactor the code to not rely on unhandledRejection for error reporting of unknown errors that happen. But it's actually really convenient to use the unhandledRejection handler to report to rollbar and shutdown the process orderly.

Please provide your exact Jest configuration and mention your Jest, node,
yarn/npm version and operating system.

OS: Mac 10.12.6
Node: v8.9.4
Yarn: 1.3.2

@JeffBaumgardt
Copy link

Not to dredge up the dead but I have a similar issue with testing unhandledrejection. Consider

window.addEventListener('unhandledrejection', event => this.onUnhandledRejection(event), {
    passive: true,
})

This code should take any unhandled promise rejection and work with it.

It works in browser but I think jsdom or jest doesn't support it. For now I have the line ignored but it would be nice to have.

OS: Windows 10
Node: 10.7.0
NPM: 6.0.0

@LukasBombach
Copy link

LukasBombach commented Sep 25, 2018

Confirming the bug with a simple example:

test('catches unhandled rejections', async () => {
  const errorHandler = jest.fn();
  const error = new Error('mock error');
  process.on('unhandledRejection', err => errorHandler(err))
  await Promise.reject(error);
  expect(errorHandler).toHaveBeenCalledWith(error);
});

will give you this

● catches unhandled rejections

mock error

  13 |
  14 | test('catches unhandled rejections', async () => {
> 15 |   const error = new Error('mock error');
     |                 ^
  16 |   const errorHandler = jest.fn();
  17 |   process.on('unhandledRejection', err => errorHandler(err))
  18 |   await Promise.reject(error);

  at Object.<anonymous>.test (test/bin.test.js:15:17)

OS: macOs 10.13.6
Node: v8.11.4 (LTS)
Yarn: 1.9.4

I also took this to StackOverflow: https://stackoverflow.com/questions/52493145/how-to-test-a-unhandledrejection-uncaughtexception-handler-with-jest

@kschat
Copy link

kschat commented Feb 20, 2019

Has anyone found a workaround for this issue?

@wearhere
Copy link

Confirmed that this breaks testing functions that throw uncaught exceptions, too: https://repl.it/repls/FreeFluffyAdministration

wearhere added a commit to mixmaxhq/day-start-interval that referenced this issue Sep 18, 2019
…`func` throws

Because, in Node, an uncaught exception will probably cause the process to
exit.

The problem preventing us testing this in Node (see the deleted test code)
turned out to be jestjs/jest#5620, although it
didn’t make sense to test that in Node after all given that the process would
exit—the interval wouldn’t continue to run regardless of how this library was
written.

We could test this in a browser environment instead, but probably better for
this library to not try to guarantee that the interval will run, as is now
documented in the code and in the README.
@sb-junhyeong
Copy link

This should be resolved because unhandledPromiseRejection might make random subsequent test fail.

@deepal
Copy link

deepal commented Jan 7, 2020

Any update on this issue yet?

@jrnail23
Copy link

Same problem here... trying to test my error handling (logging, Sentry reporting). It works fine in my other app that uses mocha for its tests, but Jest just won't let me do it.

@sadokmtir
Copy link

+1

1 similar comment
@curiosity26
Copy link

+1

@p10ns11y
Copy link

+1

1 similar comment
@snayan
Copy link

snayan commented Apr 24, 2020

+1

@pspi
Copy link

pspi commented Jun 13, 2020

Any updates on this? This would be needed to test error cases where error gets thrown after user response has already been sent.

@Jack-Works
Copy link

Yes, I want to ensure there is a unhandledPromiseRejection and I found I can't do this in Jest.

@tomapaunovic
Copy link

I can confirm this issue still happens for 'uncaughtException' as well. Currently, I don't have any workaround, other than rewrite production code to emit errors instead of throwing them, and then catch with this.on('error')

@mmarchini
Copy link

This happens because jest mocks the global process object, which prevents access to the real unhandledRejection event. I couldn't find any way to circumvent the mock or register an unhandledRejection event for a single test.

@agarnoth
Copy link

This prevents making the test fail when there is an unhandled promise in the code. It is a problem because developers are not aware there is something to fix since the test is marked successful. If there is only one test we could ask the dev to check the "UnhandledPromiseRejectionWarning" is not present in the logs before they merge, the problem is there are many tests and those warnings might be easily unseen as long as the tests are green. We then end up with many unhandled promises in the tests that might disrupt the execution of tests in our CI, and also many unhandled promises unfixed in the code, it is really bad.

After some tests, it is not possible to listen to the process events because it is not the real one (why?), we can't exploit its stdout either, the stdout of the test can't be piped :(. Even if we could exploit stdout, what we need is to fail the test to prevent the merge, so it should be done in the jest run. I haven't found any workaround. This was possible to fix in jest 24 by listening to the process "unhandledRejection" event but not possible in jest 26 because process is not the real one.

Please help?

@nesk
Copy link

nesk commented Jan 14, 2021

This is a real issue, we can't always catch exceptions because some of them might be thrown within setTimeout() or setInterval() callbacks:

setTimeout(() => { throw new Error(); }, 100);

This code above is, currently, untestable. Is there any maintainer who can help? Or at least help the community to provide a PR fixing this?

@nesk
Copy link

nesk commented Jan 17, 2021

I was able to fix this by creating a bootstrap file which runs right before Jest and stores the original process instance in a global variable! I've written a blog post about it: https://johann.pardanaud.com/blog/how-to-assert-unhandled-rejection-and-uncaught-exception-with-jest/

Once the bootstrap is setup, you will be able to write your tests just like this:

test('promise is rejected', async () => {
  const unhandledRejectionPromise = new Promise((resolve) => {
    process._original().on('unhandledRejection', resolve)
  })

  Promise.reject('test')

  await expect(unhandledRejectionPromise).resolves.toBe('test')
})

@piranna
Copy link
Contributor

piranna commented May 28, 2021

Any update? This regression has been open for more than three years... Jest 27 in Node.js 16 is dying because UnhandledPromiseRejection is not captured, while it should be done and mark the test as failed, showing the original error, not the exception about the error not being captured.

@icecream17
Copy link

icecream17 commented Jun 1, 2021

@stagas
Copy link

stagas commented Dec 21, 2021

Half a workaround is to put this inside jest.config.js, since it's being read before the process object is replaced by the (broken) mock:

const actualProcess = process
process.actual = () => actualProcess

(note it has to be a getter function as shown and not a property as it will not work for some weird reason related to the mock proxy)

Then in the test:

declare const process: any
beforeAll(() => process.actual().removeAllListeners('uncaughtException'))

...

  it('errors', () => {
    queueMicrotask(() => {
      throw new Error('some error')
    })

    return new Promise(resolve =>
      process.actual().once('uncaughtException', (error: Error) => {
        expect(error.message).toContain('some error')
        resolve(null)
      })
    )
  })

Same process for unhandledRejection i suppose.

If you want to be really nitpicky you'd want to reintroduce the listeners you removed, such as described here, which was the original idea for this trick - except we don't need to create new scripts.

@github-actions
Copy link

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale label Dec 21, 2022
@grossbart
Copy link

GitHub bot wants to close this, but it is as relevant as ever. Adding workarounds is ok-ish in our own codebase, but when contributing to other repos this is a bit much and a proper solution would be appreciated.

@antitoxic
Copy link

Is there an non-workaround/official/recommended approach for testing uncaught rejections or errors? Tagging latest contributors: @mrazauskas @SimenB ?

@Voltra
Copy link

Voltra commented Jul 21, 2023

Up

@liuxingbaoyu
Copy link
Contributor

#14315
Does this PR fix it?

ValentinViennot added a commit to ValentinViennot/jest that referenced this issue Oct 28, 2024
tested projects might require access to the original process object. might fix both jestjs#5620 and jestjs#11165 .
@zmagyar
Copy link

zmagyar commented Nov 22, 2024

#14315 Does this PR fix it?

Tested with v30.0.0-alpha.6 but it doesn't seem to solve the problem.

@yc-kanyun
Copy link

Half a workaround is to put this inside jest.config.js, since it's being read before the process object is replaced by the (broken) mock:

const actualProcess = process
process.actual = () => actualProcess
(note it has to be a getter function as shown and not a property as it will not work for some weird reason related to the mock proxy)

Then in the test:

declare const process: any
beforeAll(() => process.actual().removeAllListeners('uncaughtException'))

...

it('errors', () => {
queueMicrotask(() => {
throw new Error('some error')
})

return new Promise(resolve =>
  process.actual().once('uncaughtException', (error: Error) => {
    expect(error.message).toContain('some error')
    resolve(null)
  })
)

})
Same process for unhandledRejection i suppose.

If you want to be really nitpicky you'd want to reintroduce the listeners you removed, such as described here, which was the original idea for this trick - except we don't need to create new scripts.

it works for me

here is an code sandbox

https://codesandbox.io/p/devbox/z9qdp4?migrateFrom=zzjfzz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests