[Bug]: toThrow discards non-Error objects when used with promises, while accepting them in synchronuous code
See original GitHub issueVersion
26.4.2
Steps to reproduce
describe("set", () => {
test("syncTest", () => {
expect(
() => {throw {message: "testval"};}
)
.toThrow("testval");
});
test("asyncTest", async () => {
await expect(
async () => {throw {message: "testval"};}
).rejects
.toThrow("testval");
});
});
run these tests
Expected behavior
I expect both tests to succeed. Alternatively, I would expect both tests to fail, as they are doing the same, ± the use of Promise.
Actual behavior
Synchronous (non-promise) tests succeeds, asynchronous tests fails with a misleading error Received function did not throw
.
This happens because of https://github.com/facebook/jest/pull/5670 merged in https://github.com/facebook/jest/commit/27a1dc659dd3f2c91efb106961a5d0c640a1085b, which added a condition (a I will be linking code from latest version) https://github.com/facebook/jest/blob/01c278019726d7b01c924dd185e10b79a1f97610/packages/expect/src/toThrowMatchers.ts#L93-L95 that the rejection-reason must be an Error, https://github.com/facebook/jest/blob/01c278019726d7b01c924dd185e10b79a1f97610/packages/expect/src/utils.ts#L376-L384 otherwise the received
is plainly ignored and thrown
is kept just initialized to null, and the code path continues as if the reason was null. Later on, an internal function (in this specific case toThrowExpectedString
) is called, and see thrown
to be null and fails the matcher with the mentioned misleading error Received function did not throw
.
Note that the synchronous code path, where the received
is a function (passed to expect()) to be called and which should throw, does not have any such condition!:
https://github.com/facebook/jest/blob/01c278019726d7b01c924dd185e10b79a1f97610/packages/expect/src/toThrowMatchers.ts#L108-L112
The full code: https://github.com/facebook/jest/blob/01c278019726d7b01c924dd185e10b79a1f97610/packages/expect/src/toThrowMatchers.ts#L91-L114
Additional context
It is completely legal to throw “non-Errors” (see the implementation of the isError check above) in JS, and the toThrow Jest API does not document that the thrown object/rejection must satisfy any special conditions (currently implemented to have Error in prot chain or has an overloaded toStringTag / be a special native object to satisfy the Object.prototype.toString test).
In my case, I am working with production code that has custom exceptions that do not have Error in their prototype chain, but they are compliant with the Error (typescript) interface. Jest toThrow was working flawlessly for synchronous code with them, but I got really puzzled when I tried to use it with async code!
I propose to simply drop the isError
check, as there is no technical need for it and the presence of that check is counter intuitive.
I was reading through https://github.com/facebook/jest/pull/5670 which did not indicate any reason for this addition expect linking to older https://github.com/facebook/jest/pull/4884#issue-152281796 (to a specific comment that does not show up to me?), which also does not mention anyting about a need to limit this functionality only to “Error instances” - https://github.com/facebook/jest/pull/4884 mainly fixes the matcher itself to work correctly in promise/async variants and updates docs to use an example with Error (which I think was good because it teaches good practices) instead of string.
https://github.com/facebook/jest/pull/4884 links to an older https://github.com/facebook/jest/issues/3601 which initally added promise/async support to all matches, but it did not work correctly for toThrow.
Tagging @peterdanis , an author of https://github.com/facebook/jest/pull/5670 which caused this inconsistency.
Environment
irrelevant
Issue Analytics
- State:
- Created 2 years ago
- Reactions:3
- Comments:5 (1 by maintainers)
Top GitHub Comments
I am very sorry for any inconvenience caused. To be honest,
.toThrow
matcher does not make much sense in terms of async code, as in the end there is no way (at least I am not aware of any) to distinguish between throwing and simply rejecting the promise.Without
isError
checktoThrow
matcher would be the same astoBe
/toEqual
matchers, checking whether resolved/rejected value matches expected value (before #5670 it even worked onresolves.toThrow
).In your case you can use
rejects.toBe
/rejects.toEqual
instead, but I agree with you that I should have documented thatrejects.toThrow
chain expects an actualError
object to be thrown.First, let me thank you for a superb fast reaction!
Right, I am also not aware of a way to distinguish whether something from within async function throw an exception which triggered a rejection of the implicit promise, or if it there was a non-async function returning (in the future) rejected Promise. But with the uptake of async/await that’s exactly why I would expect
reejcts.toThrow
to work the same way as if it works with a synchronous function throwing trough call-stack.Not true AFAIK!. It actually has it’s own logic https://github.com/facebook/jest/blob/01c278019726d7b01c924dd185e10b79a1f97610/packages/expect/src/toThrowMatchers.ts#L116-L141 to compare the thrown object/rejection “reason”, based on what is pass in the “expected” value. If it’s string, it compares it with
message
property of the thrown object. If it’s object, it compares itsmessage
property with that property on the thrownobject
, there is variant with prototype check…If these are valid use-cases for “non-errors” in the synchronous
toThrow
(without isError restriction), I don’t see why they should be invalid for “non-errors” in therejects.toThrow
. Am I missing something?From the diff, I would assume that
await expect( async () => {throw new Error()} ).resolves .toThrow(Error);
still worked after it that change? If the problem is thatfromPromise
is set equally by.resolves
and.rejects
, I would suggest we make a “better fix” 👍.Frankly, I haven’t known about Jest at that time, but I still don’t see a point for a) doing change at all b) doing it just for the async case. I would make it at least less surprising if it would be done as a breaking change for the synchronous
toThrow
too back in the day…