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

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

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))

Related

Can't catch an unhandled Promise rejection from 3rd party library

I'm using a 3rd parth library to make an async call in my nodejs backend code. There's an unhandled promise rejection coming from this call which I'm having trouble catching. (It brings down my node app.)
Based on the input passed, it's expected to fail sometimes:
exports.getSomeData = (input) => {
console.log('GETTING DATA...')
return ThirdPartyLib.makeAsyncCall(input).then((result) => {
console.log('SUCCESS');
return result;
},(rejection) => {
console.log('REJECTED');
return {};
}).catch(error => {
console.log('ERROR');
return {};
});
}
But none of the REJECTED/ERROR messages print when it fails. I just see a console message from the lib: Unhandled rejection at: Promise and my app goes down.
Am I missing anything in the way I handle the non-happy path?
Is it possible for the library code to do something that the above WOULDN'T catch?
Is it possible for the library code to do something that the above WOULDN'T catch?
Sure, it just has to create a Promise (e.g. by calling an async function) without chaining it into the promise chain (aka without awaiting it):
async makeAsyncCall(data) {
Promise.reject("Possible");
}
So yes, the library you are using should either await that detached promise or it should attach an error handler to it (and handle / purposely ignore the error). It's not your fault.
Aside from the double rejection handler (which is pointless, just use catch()), your code should just work.
So given that you are seeing that error, this means that the library you are using has a bug.

Any problem with catch rejected promise and turn it into resolved with error code?

I am working on a project that always catch rejected promise, turns it into resolved one with error code, like this
return new Promise((resolve, reject) => {
axios({
...
}).then(res => {
resolve(res);
}).catch(err => {
let res = { ... }
res.error = err
resolve(res)
})
})
Then whenever call this api, rejected case is handled like this, i.e. without catch
axios_call(...).then(res => {
if (!res.err) {
//resolve case
} else {
//reject case
}
})
I never handle the rejected promise like this before so I am not sure will it cause any problem or it is just a different code style and works fine too.
I checked the possible duplicated q/a What is the explicit promise construction antipattern and how do I avoid it?
But I believed they are not the same. Because my question is about handling the rejected promise while that question is about deferred object, e.g. errors and rejections are not swallowed in my case.
First of all, avoid the Promise constructor antipattern! Your axios_call code should better look simply like this:
return axios({
...
}).catch(err => {
return { ..., error: err };
});
Then whenever call this api, rejected case is handled without catch. I never had handled the rejected promise like this before so I am not sure will it cause any problem or it is just a different code style and works fine too.
It works, but it's not fine. This style of error handling is weird and really non-idiomatic. It has the same problems as the traditional node-style callback API with separate error and result parameters:
the promise can't know whether you handled the error or not. You will not get any unhandled rejection warnings.
you always must write the code to deal with the res.error, if you want it or not. With normal promise usage, you could just supply separate onFulfill and onReject callbacks, and omitting the latter will get you the sensible default behaviour of forwarding the error instead of just dropping it.
I cannot see any advantages that your style would present, so I would recommend to avoid it and use normal promise rejections.

Promise reject() causes "Uncaught (in promise)" warning

Once a promise reject() callback is called, a warning message "Uncaught (in promise)" appears in the Chrome console. I can't wrap my head around the reason behind it, nor how to get rid of it.
var p = new Promise((resolve, reject) => {
setTimeout(() => {
var isItFulfilled = false
isItFulfilled ? resolve('!Resolved') : reject('!Rejected')
}, 1000)
})
p.then(result => console.log(result))
p.catch(error => console.log(error))
Warning:
Edit:
I found out that if the onRejected handler is not explicitly provided to the .then(onResolved, onRejected) method, JS will automatically provide an implicit one. It looks like this: (err) => throw err. The auto generated handler will throw in its turn.
Reference:
If IsCallable(onRejected)` is false, then
Let onRejected be "Thrower".
http://www.ecma-international.org/ecma-262/6.0/index.html#sec-performpromisethen
This happens because you do not attach a catch handler to the promise returned by the first then method, which therefore is without handler for when the promise rejects. You do have one for the promise p in the last line, but not for the chained promise, returned by the then method, in the line before it.
As you correctly added in comments below, when a catch handler is not provided (or it's not a function), the default one will throw the error. Within a promise chain this error can be caught down the line with a catch method callback, but if none is there, the JavaScript engine will deal with the error like with any other uncaught error, and apply the default handler in such circumstances, which results in the output you see in the console.
To avoid this, chain the .catch method to the promise returned by the first then, like this:
p.then( result => console.log('Fulfilled'))
.catch( error => console.log(error) );
Even if you use Promises correctly: p.then(p1).catch(p2) you can still get an uncaught exception if your p2 function eventually throws an exception which you intend to catch using a mechanism like window.onerror. The reason is that the stack has already been unwound by the error handling done in the promise. To fix this, make sure that your error code (called by the reject function) does not throw an exception. It should simply return.
It would be nice if the error handling code could detect that the stack has already been unwound (so your error call doesn't have to have a flag for this case), and if anyone knows how to do this easily I will edit this answer to include that explanation.
This code does not cause the "uncaught in promise" exception:
// Called from top level code;
// implicitly returns a Promise
testRejectCatch = async function() {
// Nested within testRejectCatch;
// simply rejects immediately
let testReject = function() {
return new Promise(function(resolve, reject) {
reject('test the reject');
)};
}
//***********************************************
// testRejectCatch entry.
//***********************************************
try {
await testReject(); // implicitly throws reject exception
catch(error) {
// somecode
}
//***********************************************
// top level code
//***********************************************
try{
testRejectCatch() // Promise implicitly returned,
.catch((error) => { // so we can catch
window.alert('Report error: ' + error);
// must not throw error;
});
}
catch(error) {
// some code
}
Explanation:
First, there's a terminology problem. The term "catch" is
used in two ways: in the try-catches, and in the Promises.
So, it's easy to get confused about a "throw"; is it throwing
to a try's catch or to a Promise's catch?
Answer: the reject in testReject is throwing to the Promise's
implicit catch, at await testReject; and then throwing on to
the .catch at testRejectCatch().
In this context, try-catch is irrelevant and ignored;
the throws have nothing to do with them.
The .catch at testRejectCatch satisfies the requirement
that the original throw must be caught somewhere,
so you do not suffer the "uncaught in Promise..." exception.
The takeaway: throws from Promises are throws to .catch,
not to try-catch; and must be dealt-with in some .catch
Edit:
In the above code, the reject propagates up through the .catches.
If you want, you can convert over to propagating up the try-catches.
At line 17, change the code to:
let bad = '';
await testReject().catch((error) => {bad = error});
if (bad) throw bad;
Now, you've switched over to the try-catch.
I ran into this issue, but without setTimeout().
In case anyone else runs into this: if in the Promise constructor you just call reject() synchronously, then it doesn't matter how many .then() and .catch() handlers you add to the returned Promise, they won't prevent an uncaught promise rejection, because the promise rejection would happen before you
I've solved that problem in my project, it's a large enterprise one. My team is too lazy to write empty catch hundreds of times.
Promise.prototype.then = function (onFulfilled, onRejected) {
return baseThen.call(this, (x: any) => {
if (onFulfilled)
onFulfilled(x);
}, (x: any) => {
if (onRejected)
onRejected(x);
});
};

Should I refrain from handling Promise rejection asynchronously?

I have just installed Node v7.2.0 and learned that the following code:
var prm = Promise.reject(new Error('fail'));
results in this message:;
(node:4786) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: fail
(node:4786) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
I understand the reasoning behind this as many programmers have probably experienced the frustration of an Error ending up being swallowed by a Promise. However then I did this experiment:
var prm = Promise.reject(new Error('fail'));
setTimeout(() => {
prm.catch((err) => {
console.log(err.message);
})
},
0)
which results in:
(node:4860) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: fail
(node:4860) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
(node:4860) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
fail
I on basis of the PromiseRejectionHandledWarning assume that handling a Promise rejection asynchronously is/might be a bad thing.
But why is that?
Note: See 2020 updates below for changes in Node v15
"Should I refrain from handling Promise rejection asynchronously?"
Those warnings serve an important purpose but to see how it all works see those examples:
Try this:
process.on('unhandledRejection', () => {});
process.on('rejectionHandled', () => {});
var prm = Promise.reject(new Error('fail'));
setTimeout(() => {
prm.catch((err) => {
console.log(err.message);
})
}, 0);
Or this:
var prm = Promise.reject(new Error('fail'));
prm.catch(() => {});
setTimeout(() => {
prm.catch((err) => {
console.log(err.message);
})
}, 0);
Or this:
var caught = require('caught');
var prm = caught(Promise.reject(new Error('fail')));
setTimeout(() => {
prm.catch((err) => {
console.log(err.message);
})
}, 0);
Disclaimer: I am the author of the caught module (and yes, I wrote it for this answer).
Rationale
It was added to Node as one of the Breaking changes between v6 and v7. There was a heated discussion about it in Issue #830: Default Unhandled Rejection Detection Behavior with no universal agreement on how promises with rejection handlers attached asynchronously should behave - work without warnings, work with warnings or be forbidden to use at all by terminating the program. More discussion took place in several issues of the unhandled-rejections-spec project.
This warning is to help you find situations where you forgot to handle the rejection but sometimes you may want to avoid it. For example you may want to make a bunch of requests and store the resulting promises in an array, only to handle it later in some other part of your program.
One of the advantages of promises over callbacks is that you can separate the place where you create the promise from the place (or places) where you attach the handlers. Those warnings make it more difficult to do but you can either handle the events (my first example) or attach a dummy catch handler wherever you create a promise that you don't want to handle right away (second example). Or you can have a module do it for you (third example).
Avoiding warnings
Attaching an empty handler doesn't change the way how the stored promise works in any way if you do it in two steps:
var prm1 = Promise.reject(new Error('fail'));
prm1.catch(() => {});
This will not be the same, though:
var prm2 = Promise.reject(new Error('fail')).catch(() => {});
Here prm2 will be a different promise then prm1. While prm1 will be rejected with 'fail' error, prm2 will be resolved with undefined which is probably not what you want.
But you could write a simple function to make it work like a two-step example above, like I did with the caught module:
var prm3 = caught(Promise.reject(new Error('fail')));
Here prm3 is the same as prm1.
See: https://www.npmjs.com/package/caught
2017 Update
See also Pull Request #6375: lib,src: "throw" on unhandled promise rejections (not merged yet as of Febryary 2017) that is marked as Milestone 8.0.0:
Makes Promises "throw" rejections which exit like regular uncaught errors. [emphasis added]
This means that we can expect Node 8.x to change the warning that this question is about into an error that crashes and terminates the process and we should take it into account while writing our programs today to avoid surprises in the future.
See also the Node.js 8.0.0 Tracking Issue #10117.
2020 Update
See also Pull Request #33021: process: Change default --unhandled-rejections=throw (already merged and released as part of the v15 release - see: release notes) that once again makes it an exception:
As of Node.js 15, the default mode for unhandledRejection is changed to throw (from warn). In throw mode, if an unhandledRejection hook is not set, the unhandledRejection is raised as an uncaught exception. Users that have an unhandledRejection hook should see no change in behavior, and it’s still possible to switch modes using the --unhandled-rejections=mode process flag.
This means that Node 15.x has finally changed the warning that this question is about into an error so as I said in 2017 above, we should definitely take it into account while writing our programs because if we don't then it will definitely cause problems when upgrading the runtime to Node 15.x or higher.
I assume that handling a Promise rejection asynchronously is a bad thing.
Yes indeed it is.
It is expected that you want to handle any rejections immediately anyway. If you fail to do (and possibly fail to ever handle it), you'll get a warning.
I've experienced hardly any situations where you wouldn't want to fail immediately after getting a rejection. And even if you need to wait for something further after the fail, you should do that explicitly.
I've never seen a case where it would be impossible to immediately install the error handler (try to convince me otherwise). In your case, if you want a slightly delayed error message, just do
var prm = Promise.reject(new Error('fail'));
prm.catch((err) => {
setTimeout(() => {
console.log(err.message);
}, 0);
});

Why it is an error to ignore a rejected promise on Chrome?

I don't want to do anything if the promise is rejected, like getPromise().then(foo=>{});. Why is it an error on Chrome?
(new Promise((resolve, reject)=>{reject()}))
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: undefined}
VM3250:2 Uncaught (in promise) undefined
On Node and Firefox, it is OK to ignore the rejected part.
Promise rejection is like uncaught exception.
if you want to ignore exception - catch it, but don't handle,
same here - add .catch statement, but do nothing
Promise.reject(new Error('bad..')).catch(e => {})
I would not however recommend that, as promises reject for a reason, so you might want to add some sort of handling logic
Without a promise reject handler, chrome would throws reason of the rejection asynchronously that doesn't effects any JavaScript process, just prints the reason to console. If you get annoyed with this, you need to add handler, e.g. Promise.reject(reason).catch(reason => {}).
UPDATE: Why is an error? It may be because rejection is conventionally considered as an exception (an error).
P.S. Your "question" look like a feedback rather than a question. I personally prefer getting the reason as an error.
Rejecting a promise basically means something bad happened, and that you should be handling it with a call to catch. If you reject a promise without having a catch, it will throw an exception (more accurately, an unhandled rejection)
var a = (new Promise((resolve, reject) => {
reject("my error")
})).catch((err) => {
console.error(err)
})
I'm guessing it is a specificity of V8 if this happens only under Chrome, but it makes sense to me

Categories

Resources