Jest: Jest does not allow asynchronous catching of rejected promises

Created on 18 Apr 2018  路  8Comments  路  Source: facebook/jest

Bug
Jest version: 22.4.3 (But was introduced in 21.x)
Config: None (all defaults)
Node version: 6.11.5, 8.9.4

Jest does not allow you to asynchronously attach a .catch() to a promise, even if it's added before the test itself is finished. The only difference between these two tests is that the catch is added within a setTimeout() in the second one.

// This passes after ~107ms (the expected amount of time)
test('Synchronous catching of promise', (done) => {
    const promise = Promise.reject(new Error('nope'));

    promise.catch((err) => console.log('Caught error: ', err.message));

    // At a later time, finish the test
    setTimeout(() => {
        console.log('here');
        done();
    }, 100);
});

// This fails after ~10ms, with error message "nope"
test('Async catching of promise', (done) => {
    const promise = Promise.reject(new Error('nope'));

    setTimeout(() => {
        promise.catch((err) => console.log('Caught error: ', err.message));
    });

    // At a later time (well after the catch() has been attached to the promise), finish the test
    setTimeout(() => {
        // We nver makes it to this point because Jest sees that promise was not caught immediately
        console.log('here');
        done();
    }, 100);
});

screen shot 2018-04-18 at 4 40 26 pm

In Jest 20.x, only warnings are printed, and the test does not fail

(node:54613) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: nope
(node:54613) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 2)

(Was originally mentioned in https://github.com/facebook/jest/issues/3251)

Most helpful comment

Nice idea! I reworked it into a defuse function:

function defuse(promise) {
  promise.catch(() => {});
  return promise;
}

it('terminates the test prematurely', async () => {
  const promise = defuse(new Promise((resolve, reject) => setTimeout(() => reject(new Error('Oops')), 0)));
  await new Promise(resolve => setTimeout(resolve, 10));
  await expect(promise).rejects.toThrow('Oops');
});

Still, this is a workaround for a problem that I believe should be fixed.

All 8 comments

Duplicate of #5311 (although this has a better description 馃檪)

@SimenB this is not a duplicate. #5311 is about a warning and what looks to me like a misunderstanding. In @gaearon's code, expect() will throw out of the test and leave promise dangling, hence garbage collected and warned about it not being caught. That's actually good.

This issue is a serious bug, it means one can't write code which asynchronously catches promise rejections. This is entirely unexpected.

Going back to jest 20 from 24 solves this, so it's obviously a regression.

It has the same underlying cause - we added our own 'unhandled rejection' handler. The change is a good one, as a random unhandled promise will fail your test (like a sync error) instead of either being swallowed or just printed as a warning. I think it's way more more often the case you want to fail tests on unhandled promise rejections than testing handlers. _However_, I think we should allow disabling it on a test by test basis if you explicitly want to test rejection handlers.

I think that can be tracked in #5311, or do you think it's orthogonal?

I'm having the same problem. Consider the following test:

it('terminates the test prematurely', async () => {
  const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Oops')), 0));
  await new Promise(resolve => setTimeout(resolve, 10));
  await expect(promise).rejects.toThrow('Oops');
});

This creates a promise, does some asynchronous stuff, then expects that the promise fails. But execution never reaches the assertion. As soon as the promise rejects (while waiting for the timeout), the unit test is terminated and marked as failing.

I understand that #5311 has a similar cause, but I believe these are two different issues. My understanding is that #5311 describes the case that at the end of the test, promises are still dangling. This should be avoided. This issue, in contrast, describes the case that during test execution, a promise is not immediately subscribed to. This, in my opinion, is a perfectly valid scenario that should work.

What's more, the code may not even be part of the test suite, but of the system under test. Consider:

code-under-test.js

export async function codeUnderTest() {
  const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Oops')), 0));
  await new Promise(resolve => setTimeout(resolve, 10));
  return promise;
}

test.js

import { codeUnderTest } from '../src/code-under-test.js';

describe('codeUnderTest', () => {
  it('terminates the test prematurely', async () => {
    await expect(codeUnderTest()).rejects.toThrow('Oops');
  });
});

The function codeUnderTest is perfectly valid, yet fails any unit test that attempts to call it. Is there any workaround?

After much frustration, we were able to work around this issue by adding a dummy catch to the promise.

it('terminates the test prematurely', async () => {
  const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Oops')), 0));
  promise.catch(err => { return; });
  await new Promise(resolve => setTimeout(resolve, 10));
  await expect(promise).rejects.toThrow('Oops');
});

This will not solve the issue in an imported function, but should at least band-aid the problem in tests.

Nice idea! I reworked it into a defuse function:

function defuse(promise) {
  promise.catch(() => {});
  return promise;
}

it('terminates the test prematurely', async () => {
  const promise = defuse(new Promise((resolve, reject) => setTimeout(() => reject(new Error('Oops')), 0)));
  await new Promise(resolve => setTimeout(resolve, 10));
  await expect(promise).rejects.toThrow('Oops');
});

Still, this is a workaround for a problem that I believe should be fixed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jardakotesovec picture jardakotesovec  路  3Comments

ticky picture ticky  路  3Comments

stephenlautier picture stephenlautier  路  3Comments

hramos picture hramos  路  3Comments

mmcgahan picture mmcgahan  路  3Comments