javascriptnode.jspromisesinonchai-as-promised

Sinon, observing different behaviour using Promise.reject() to stub.rejects()


I was having issues running a test (in Node),

I was simulating a promise being rejected, and my code should retry (using promise-retry if that could be relevant).

When I simulated the rejected promise using stub.returns(Promise.reject(error)

I was getting a uncaught error warnings (for my dummyErrors), even though I am catching errors where I call my function...

-note, these uncaught errors were only happening in the unit tests not in real calls.

const mockedFunction = sinon.stub();

const dummyError = new Error('Document is locked');

mockedFunction.onCall(0).returns(Promise.reject(dummyError));
mockedFunction.onCall(0).returns(Promise.reject(dummyError));
mockedFunction.onCall(0).returns(Promise.reject(dummyError));
mockedFunction.onCall(1).returns(Promise.resolve({approved: true}));

I discovered that by changing to use the stub.rejects() syntax:

mockedFunction.onCall(0).rejects(dummyError);
mockedFunction.onCall(1).rejects(dummyError);
mockedFunction.onCall(2).rejects(dummyError));
mockedFunction.onCall(3).resolves({approved: true});

I no longer get the uncaught error warnings.

My issue is solved, however I would like to get a better understand as to why, I looked at the sinon source code and it looks like the implementation of .rejects is no different


Solution

  • In promise implementations that are intrinsically detect uncaught error (including V8/Node.js), an error from rejected promise should be caught on same tick, otherwise it triggers UnhandledPromiseRejectionWarning.

    This will work fine:

    let promise = Promise.reject();
    promise.catch(() => {});
    

    This will result in potentially unhandled promise and trigger a warning:

    let promise = Promise.reject();
    setTimeout(() => {
      promise.catch(() => {});
    });
    

    If Promise.reject(dummyError) isn't chained with catch(...) or then(..., ...) on same tick, it triggers a warning, while in case of rejects(dummyError) a rejected promise is created on function call, so this likely will be true:

    sinon.spy(Promise, 'reject');
    mockedFunction.onCall(0).rejects(dummyError);
    expect(Promise.reject).to.have.not.been.called;
    mockedFunction();
    expect(Promise.reject).to.have.been.called;
    

    An alternative to rejects(dummyError) is:

    mockedFunction.onCall(0).callsFake(() => Promise.reject(dummyError))