jest: Jest gives no indication that test fails because of unhandled promise rejection.

šŸ› Bug Report

When Jest fails a test for what it thinks - wrongly - is an unhandled rejection, it does not indicate this at all.

To Reproduce

This code in a test module causes Jest to wrongly think a promise rejection isnā€™t going to be handled.

describe("whatever", () => {
  it("fails for no reason", async () => {
    const p = Promise.reject(new Error("Shouldn't fail the test"));
    await new Promise(r => setTimeout(r, 100));
    await expect(p).rejects.toMatchObject({
      message: "Shouldn't fail the test",
    });
  });
});

Output:

 FAIL  src/test/plugging-in.test.ts
  DeviceManager - plugging and unplugging
    ā—‹ skipped becomes useful after being plugged in
  whatever
    Ɨ fails for no reason (12ms)

  ā— whatever ā€ŗ fails for no reason

    an error message

      90 | describe("whatever", () => {
      91 |   it("fails for no reason", async () => {
    > 92 |     const p = Promise.reject(new Error("an error message"));
         |                              ^
      93 |     await new Promise(r => setTimeout(r, 100));
      94 |     await expect(p).rejects.toMatchObject({
      95 |       message: "an error message",

      at Object.it (src/test/plugging-in.test.ts:92:30)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 skipped, 2 total
Snapshots:   0 total
Time:        4.355s, estimated 6s
Ran all test suites matching /plugging-in/i with tests matching "whatever".

Note that nothing in the output conveys the real reason this test failed.

Expected behavior

Something in the output says that Jest failed the test because it canā€™t guarantee that the promise rejection will be handled. For instance, instead of:


     an error message

something like:

    Unhandled Promise Rejection:
    an error message

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 33
  • Comments: 25 (7 by maintainers)

Most upvoted comments

I would also like to add that the problem here is not only ā€œno indicationā€, but also the fact that the test fails: I was writing a test for utility function and I need to test that this function handles both successful and failing callbacks passed to it. Seems like the ā€œnegativeā€ test fails for me anyway, even with expect(promise).rejects...., try/catch.

P.S. Also if I add const promise = Promise.reject('aaaa') to the top level in the file (outside describe) Jest will randomly fail first two tests. Very weirdā€¦

@JoeLangewayClearā€™s workaround will suppress these errors, but itā€™s kind of like disabling the engine fault light in your car and claiming you ā€œfixed itā€

image

In node v16 the issue has gotten a lot worse.

[Nest] 36135  - 17/01/2022, 10:29:12   DEBUG Auto-corrected AuditLogServiceClient 'baseUrl' value: Removed /api/v1.
[Nest] 36135  - 17/01/2022, 10:29:12   DEBUG Auto-corrected AuditLogServiceClient 'baseUrl' value: Removed /api/v1.
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "TypeError: promise.then is not a function".] {
  code: 'ERR_UNHANDLED_REJECTION'
}
end of process....

Iā€™ve tested the earlier mentioned reproduction case and it seems to still be an issue in Node v20.10.0, Jest v29.3.1.

Looks like it goes wrong in promises & async nesting only?

function _rejecting() {
	return new Promise((resolve, reject) => {
		reject(new Error('An error'));
	});
}

test("this fails", async () => {
	const promise = (function _testMe() {
		return new Promise(async () => {
			await _rejecting();
		});
	})();

	return expect(promise).rejects.toThrowError();
});

test("this doesn't fail", async () => {
	const promise = (async function _testMe() {
		await _rejecting();
	})();

	return expect(promise).rejects.toThrowError();
});

Very minimal reproduction case. If the rejected promise is nested, it fails the test, if it isnā€™t it wonā€™t. And the promise isnā€™t exactly unhandled, as the async should be chaining them.

The code might look a bit weird like that, but itā€™s sometimes needed to mix async & promises with things like manual timeouts. This is just a minimal repro.

Oh wow ok Iā€™ve just spent far too long trying to figure out how Jest was even seeing the error it was reporting. Really could do with jest saying something, anything, about how it caught the error.

Unhandled error/rejection bugs can be subtle and hard to track down, and often appear when you end up with the worst case async scenario where code is unintentionally executing in the background (e.g. missing return Promise) where it canā€™t be sequenced and errors canā€™t be handled.

Having Jest show the error without indicating anything at all about how it caught the error is very confusing, especially since Jest seems to override/ignore any unhandled rejection/exception handlers you add in test.

Any updates about this bug? this should be critical, itā€™s affecting the coverage in our project (and I believe everybodyā€™s) and there is no way to work around it except to just not test it.

Here is a comment to show, that this issue is still relevant. Very much so!

Hitting this too. There should be a setting to disable this functionality.

I tried editing jest-circus/build/globalErrorHandlers.js in place, commenting out the inject and restore, and then jest crashes at the site of the uncaught error that later gets caught.

So maybe itā€™s actually a problem with nodeā€™s detection being too eager?

In any case, the function uncaught could already log the full stack trace of the error, saying itā€™s uncaught.

A workaround for others encountering this problem, or rather, a work around for the difficulty of Jest not liking unhandled rejections:

/**
 * A function to wrap the Promise constructor such that no rejections are considered unhandled
 * by either Node or Jest. Whatever handlers can still be attached and rejections turned into
 * thrown exceptions where ever the returned promise is awaited.
 */
function makePromise(callback) {
  const promise = new Promise(callback);
  promise.catch(()=>{});
  return promise;
}

Here is a comment to show, that this issue is still relevant. Very much so!

šŸ‘šŸ½

I created a PR that at least marks uncaught errors with their reason, but it doesnā€™t explain why Iā€™m getting uncaught errors that are clearly caught.

When I replace the uncaught function with the below, my tests pass, and my app works just fine. This should be a command line option.

const uncaught = error => {
	console.error(new Error(`uncaught (but ignoring): ${error.stack}`))
	return
	;(0, _state.dispatchSync)({
		error,
		name: 'error',
	})
}