Getting a UnhandledPromiseRejectionWarning when testing using mocha/chai
Asked Answered
D

8

169

So, I'm testing a component that relies on an event-emitter. To do so I came up with a solution using Promises with Mocha+Chai:

it('should transition with the correct event', (done) => {
  const cFSM = new CharacterFSM({}, emitter, transitions);
  let timeout = null;
  let resolved = false;
  new Promise((resolve, reject) => {
    emitter.once('action', resolve);
    emitter.emit('done', {});
    timeout = setTimeout(() => {
      if (!resolved) {
        reject('Timedout!');
      }
      clearTimeout(timeout);
    }, 100);
  }).then((state) => {
    resolved = true;
    assert(state.action === 'DONE', 'should change state');
    done();
  }).catch((error) => {
    assert.isNotOk(error,'Promise error');
    done();
  });
});

On the console I'm getting an 'UnhandledPromiseRejectionWarning' even though the reject function is getting called since it instantly shows the message 'AssertionError: Promise error'

(node:25754) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): AssertionError: Promise error: expected { Object (message, showDiff, ...) } to be falsy

  1. should transition with the correct event

And then, after 2 sec I get

Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

Which is even weirder since the catch callback was executed(I think that for some reason the assert failure prevented the rest of the execution)

Now the funny thing, if I comment out the assert.isNotOk(error...) the test runs fine without any warning in the console. It stills 'fails' in the sense that it executes the catch.
But still, I can't understand these errors with promise. Can someone enlighten me?

Dromedary answered 27/9, 2016 at 5:23 Comment(2)
I think you have one extra set of closing brace and parens at the very last line. Please delete them and try again.Rybinsk
This is so cool, the new unhandled rejection warning finds bugs in real life and saves people time. So much win here. Without this warning your tests would have timed out without any explanation.Elitism
N
191

The issue is caused by this:

.catch((error) => {
  assert.isNotOk(error,'Promise error');
  done();
});

If the assertion fails, it will throw an error. This error will cause done() never to get called, because the code errored out before it. That's what causes the timeout.

The "Unhandled promise rejection" is also caused by the failed assertion, because if an error is thrown in a catch() handler, and there isn't a subsequent catch() handler, the error will get swallowed (as explained in this article). The UnhandledPromiseRejectionWarning warning is alerting you to this fact.

In general, if you want to test promise-based code in Mocha, you should rely on the fact that Mocha itself can handle promises already. You shouldn't use done(), but instead, return a promise from your test. Mocha will then catch any errors itself.

Like this:

it('should transition with the correct event', () => {
  ...
  return new Promise((resolve, reject) => {
    ...
  }).then((state) => {
    assert(state.action === 'DONE', 'should change state');
  })
  .catch((error) => {
    assert.isNotOk(error,'Promise error');
  });
});
Nutritive answered 27/9, 2016 at 7:27 Comment(7)
For anyone curious, this is also true for jasmine.Cryolite
@Nutritive Won't catch will get called for any error and not just the one you're expecting? I think this style won't work if you're trying to assert failures.Servomechanism
@Servomechanism the catch handler should probably be passed as second argument to then. However, I'm not entirely sure what the intention of the OP was, so I left it as-is.Nutritive
And also for anyone curious about jasmine, use done.fail('msg') in this case.Coadjutor
can you give full example of where main code vs assertions should go, not so clear here.....especially if you asserting a value from the original service/promise call in our code. Does the actual promise for our code go within this other "mocha promise" ??Gilliangilliard
@Gilliangilliard perhaps it helps if consider your test case as a .then handler: you return a promise chain from it, and typically, that chain starts with the function under test, followed by a .then where you will assert that the value(s) returned are correct. See this gist.Nutritive
To use the default behavior provided by mocha, simply return the promise with no catch. It will mark the test as failed if the promise is rejected and pretty print the error.Neva
D
19

For those who are looking for the error/warning UnhandledPromiseRejectionWarning outside of a testing environment, It could be probably because nobody in the code is taking care of the eventual error in a promise:

For instance, this code will show the warning reported in this question:

new Promise((resolve, reject) => {
  return reject('Error reason!');
});

(node:XXXX) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Error reason!

and adding the .catch() or handling the error should solve the warning/error

new Promise((resolve, reject) => {
  return reject('Error reason!');
}).catch(() => { /* do whatever you want here */ });

Or using the second parameter in the then function

new Promise((resolve, reject) => {
  return reject('Error reason!');
}).then(null, () => { /* do whatever you want here */ });
Dobbs answered 5/4, 2018 at 4:53 Comment(2)
Of course but I think in real-life we usually do not use just new Promise((resolve, reject) => { return reject('Error reason!'); }) but in function function test() { return new Promise((resolve, reject) => { return reject('Error reason!'); });} so inside function we do not need to use .catch() but to successfully handle errors it is enough to use when calling that function test().catch(e => console.log(e)) or async/await version try { await test() } catch (e) { console.log(e) }Dendrology
I fixed the same error this way, by adding a missing catch block during database connection.Perez
V
11

I got this error when stubbing with sinon.

The fix is to use npm package sinon-as-promised when resolving or rejecting promises with stubs.

Instead of ...

sinon.stub(Database, 'connect').returns(Promise.reject( Error('oops') ))

Use ...

require('sinon-as-promised');
sinon.stub(Database, 'connect').rejects(Error('oops'));

There is also a resolves method (note the s on the end).

See http://clarkdave.net/2016/09/node-v6-6-and-asynchronously-handled-promise-rejections

Varityper answered 21/10, 2016 at 4:52 Comment(1)
Sinon now includes the methods "resolves" and "rejects" for stubs as of version 2. See npmjs.com/package/sinon-as-promised. I still +1'ed the answer, though - I didn't know about this.Difficult
C
11

The assertion libraries in Mocha work by throwing an error if the assertion was not correct. Throwing an error results in a rejected promise, even when thrown in the executor function provided to the catch method.

.catch((error) => {
  assert.isNotOk(error,'Promise error');
  done();
});

In the above code the error objected evaluates to true so the assertion library throws an error... which is never caught. As a result of the error the done method is never called. Mocha's done callback accepts these errors, so you can simply end all promise chains in Mocha with .then(done,done). This ensures that the done method is always called and the error would be reported the same way as when Mocha catches the assertion's error in synchronous code.

it('should transition with the correct event', (done) => {
  const cFSM = new CharacterFSM({}, emitter, transitions);
  let timeout = null;
  let resolved = false;
  new Promise((resolve, reject) => {
    emitter.once('action', resolve);
    emitter.emit('done', {});
    timeout = setTimeout(() => {
      if (!resolved) {
        reject('Timedout!');
      }
      clearTimeout(timeout);
    }, 100);
  }).then(((state) => {
    resolved = true;
    assert(state.action === 'DONE', 'should change state');
  })).then(done,done);
});

I give credit to this article for the idea of using .then(done,done) when testing promises in Mocha.

Canthus answered 9/6, 2017 at 20:54 Comment(0)
A
2

I faced this issue:

(node:1131004) UnhandledPromiseRejectionWarning: Unhandled promise rejection (re jection id: 1): TypeError: res.json is not a function (node:1131004) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.j s process with a non-zero exit code.

It was my mistake, I was replacing res object in then(function(res), so changed res to result and now it is working.

Wrong

module.exports.update = function(req, res){
        return Services.User.update(req.body)
                .then(function(res){//issue was here, res overwrite
                    return res.json(res);
                }, function(error){
                    return res.json({error:error.message});
                }).catch(function () {
                   console.log("Promise Rejected");
              });

Correction

module.exports.update = function(req, res){
        return Services.User.update(req.body)
                .then(function(result){//res replaced with result
                    return res.json(result);
                }, function(error){
                    return res.json({error:error.message});
                }).catch(function () {
                   console.log("Promise Rejected");
              });

Service code:

function update(data){
   var id = new require('mongodb').ObjectID(data._id);
        userData = {
                    name:data.name,
                    email:data.email,
                    phone: data.phone
                };
 return collection.findAndModify(
          {_id:id}, // query
          [['_id','asc']],  // sort order
          {$set: userData}, // replacement
          { "new": true }
          ).then(function(doc) {
                if(!doc)
                    throw new Error('Record not updated.');
                return doc.value;   
          });
    }

module.exports = {
        update:update
}
Axil answered 3/5, 2017 at 19:22 Comment(0)
C
1

Here's my take experience with E7 async/await:

In case you have an async helperFunction() called from your test... (one explicilty with the ES7 async keyword, I mean)

→ make sure, you call that as await helperFunction(whateverParams) (well, yeah, naturally, once you know...)

And for that to work (to avoid ‘await is a reserved word’), your test-function must have an outer async marker:

it('my test', async () => { ...
Carlyn answered 30/10, 2017 at 16:27 Comment(2)
You do not have to call it as await helperFunction(...). An async function returns a promise. You could just handle the returned promise like you'd do on a function not marked async that happens to return a promise. The point is to handle the promise, period. Whether the function is async or not does not matter. await is merely one among multiple ways to handle the promise.Ampoule
True. But then I have to invest lines on catching... or my tests pass as false positives, and any failing promises will go unoticed (in terms of testrunner results). So await looks like less lines&effort to me. – Anyway, forgetting await was, what caused that UnhandledPromiseRejectionWarning for me... thus this answer.Carlyn
S
0

I had a similar experience with Chai-Webdriver for Selenium. I added await to the assertion and it fixed the issue:

Example using Cucumberjs:

Then(/I see heading with the text of Tasks/, async function() {
    await chai.expect('h1').dom.to.contain.text('Tasks');
});
Standifer answered 31/12, 2019 at 0:39 Comment(0)
A
0

Just a heads-up that you can get a UnhandledPromiseRejectionWarning if you accidentally put your test code outside of the it-function. 😬

    describe('My Test', () => {
      context('My Context', () => {
        it('should test something', () => {})
        const result = testSomething()
        assert.isOk(result)
        })
      })
Asquith answered 3/3, 2021 at 8:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.