New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide cover for unhandled rejections #3486
Conversation
… turn into unhandled rejections
Hey, just wanted to share– there are two things that can help you think about this and code around this in the interim.
My impression is that there is not any intention of porting this feature into hapi land, but we shall see :) |
Thanks @devinivy, I like the tips! I'll look into both as an interim solution. I'd like to stay as close to hapi semantics as I can and avoid wrapping it with other code, but definitely have to try and find a way to reduce the likelihood of those unhandled rejections occurring. I'm a fan of promises in how they can make some code a little easier to reason about, but the extra effort required to make sure people are getting all of the error conditions right is a nightmare! |
Can you not add a |
Hi @AdriVanHoudt, The challenge with this is that there is no way to then actually call |
Ok but normally you should catch this in testing no? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely approve of handling returned Promise
rejection values, like you suggest. This should mirror the way that regular throw
's in the handler function are handled.
lib/handler.js
Outdated
|
||
// Give hapi a fighting chance to deal with uncaught rejections. | ||
if (Promises.isThennable(handlerResult)) { | ||
handlerResult.catch(reply); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably better to use .then(null, reply)
instead of catch()
, since we haven't tested that the catch()
method exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also needs to handle rejections where the object is not an instanceof Error
, like this code:
Lines 471 to 480 in b4aeaf7
const onError = (source) => { | |
if (!(source instanceof Error)) { | |
const err = new Error('Rejected promise'); | |
err.data = source; | |
return next(Boom.wrap(err)); | |
} | |
return next(Boom.wrap(source)); | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @kanongil yes it needs to use .then
, goofy mistake on my side. For the second point that's a great spot, am checking on this now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed, also added support for wrapping the object in an error needed to maintain parity with the unhandled exceptions. would welcome advice on whether you think the logic to wrap an error in a boom object is general enough to extract into its own function to avoid duplication of logic @kanongil. It's a few line and might be clearer left inline, but am happy to extract it (perhaps into promise.js
as a boomReject
function or something)
API.md
Outdated
return badPromise() | ||
.then((result) => { reply(result); }); | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bad indentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On it
API.md
Outdated
|
||
new Promise(() => { | ||
|
||
setTimeout(() => throw new Error(), 1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setTimeout
seems redundant, and confuses the example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's to show that errors thrown from asynchronous functions inside Promises are caught and handled correctly. By extension, synchronous errors will be caught and handled correctly too. (?) Happy to be corrected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kanongil @AJamesPhillips yes that's correct, it's to make it clear and explicit that this method will work even if exception is thrown in a genuinely async context (a nextTick
could be used if the fact that it is a timeout might be misleading people into thinking it is a timing related issue)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe there's something I'm missing here, but unless there's some magic going on behind the scenes with domains this should result in resolving with undefined
and triggering an uncaughtException
on the process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me make sure the test case is exactly right for this to confirm everything, working on the changes now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimBeyer thanks for the heads up, you are quite right. In this case the following is hit:
https://github.com/hapijs/hapi/blob/master/lib/protect.js#L55
And so this case is handled. This feature will only solve for unhandled rejections and exceptions thrown synchronously in a promise constructor, I'm updating the tests to correctly reflect this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a test case showing that the setTimeout
example is already handled by the protect
mechanism, to keep things clearer. Also attempted to make the api documentation sharper.
API.md
Outdated
} | ||
|
||
// You *should* catch the rejection yourself! But hapi will | ||
// return the error for you... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? You should only need to catch it, if you want to handle it in a special way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll make the comment clear, what I meant was that if you are orchestrating a promise chain, you should pretty much always use a catch (unless you return the promise to something else which can catch it). In this case as hapi would be able to catch it, the comment is misleading
@AdriVanHoudt agreed, tests should cover these scenarios, but I think it still adds a layer of safety as even for experienced developers it can be easy to miss an edge case with a promise |
|
||
return new Promise((resolve, reject) => { | ||
|
||
throw new Error('This should be rejected...'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add a test for your setTimeout(() => throw new Error(), 1000);
too from above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call! Adding now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the test, it's there now :)
…o cover all relevant error handling cases.
Hi @AdriVanHoudt yes it now goes through the same process as with callbacks, transforming the error by wrapping it in Boom as needed, so the behaviour of both is very close now 😄 |
handlerResult.then(null, (error) => { | ||
|
||
// Unhandled rejections are always Boom-ified, just like uncaught exceptions. | ||
if (error instanceof Error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it common practice to reject a promise with a non-Error object?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i sure hope not..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You certainly aren't supposed to, but out in the wild it happens. As to whether hapi should deal with the rejection of a non-error, I can't say. I would probably treat it the same way hapi deals with a non-error thrown in a handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, can happen. Have seen strings thrown in the past.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hueniverse it is definitely non-conventional to reject
a non-error object. If done deliberately it would definitely be an anti-pattern. However, there are cases where it will slip in by mistake, here's the most common scenario:
function doSomethingAsynchronous(input) {
return new Promise((resolve, reject) => {
callbackToLibrary(input, (err, result) => {
if (err) return reject(err); // uh-oh....
return resolve(result);
});
});
}
In this case, if the library you are using does something goofy like callback with a non-error object, the promise will reject with the non-error object.
I think that with the handling of thrown exceptions in domains we have the same behaviour (because Boom.wrap
covers whatever is thrown, ensuring that even if what is thrown is not an error, it gets wrapped in one).
Should this be the default behavior? Do people who use promises consider this a breaking change? |
Just one user here but I do use promises with hapi. I would like this as the default behavior but would consider it a breaking change :/ |
i'm curious why you would consider this a breaking change @corbinu. are you using a handler somewhere in the request lifecycle to intercept promise rejections somehow? that's the only situation i can think of off the top of my head that would cause this patch to lead to undesirable results. i'm definitely all ears if i'm missing something, though. |
I am not but assumed somebody might be you guys know your users better than I do though :) |
just checking, thought maybe you had a specific use case in mind. i personally can't think of one, and unless someone else can IMO i'm not sure i'd call this a breaking change. to me this is very similar to having handlers run through domains, the intent isn't to alter behavior, but to make sure that a response is sent even if an unexpected failure occurs. some sort of error handling plugin is the only thing i can think of this change would interfere with, and an error handling plugin that only catches rejected promises seems like the sort of thing a user would be happy to get rid of in favor of native support. TL;DR - unless someone has a strong objection or legitimate use case for the existing behavior, i don't believe this is a breaking change and i do believe it's a good idea to enable by default |
I agree that this isn't a breaking change. If you're already catching your own rejections, that should still work just fine. |
@nlf that's essentially the idea, that we get a similar opportunity to cover unhandled rejections in the same way we can with uncaught exceptions in domains. In Node 8 domains will be aware of native unhandled rejections interestingly (which should make it easier to be resilient in this situation, even if the user doesn't @devinivy yep, if you do:
Then the extra |
@dwmkerr not exactly. It just executes the functions passed to |
I was using generators with a different framework and am now using promises and async / await with Hapi. I would love to see better built-in support for unhandled rejections. I am currently using
This isn't the best implementation, but it works. However, with this PR, it looks like this won't be necessary anymore. Correct? It looks like you're doing something similar, but at the framework level -- which is great. 👍 Just wondering if I'm missing anything or if I'll be able to get rid of the above code once this lands. |
@rubennorte thanks for the clarification! |
@jeff-kilbride Correct, you will no longer need the code shown, however there is one subtle caveat. With your code, if a handler does this: function handler(request, reply) {
return new Promise((reply, reject) => {
throw "The cake is a lie";
});
} Your current work-around will lead to In short: Before: This is to maintain parity with how hapi handles uncaught exceptions (wrapping them in errors). |
@jeff-kilbride I am doing something similar and it is working fine. The only difference is that my handler method is not static. |
@cur3n4 Thanks for the heads up! It looks like
I have
How do I get |
In my package.json I have glue ^4.1.0. That did it for me. |
I found it. I needed to rebuild my docker containers after upgrading hapi. 🙄 Looks like everything is working as it should, now, without my extension! 🎉 @cur3n4 Thanks for the push in the right direction! I'm deleting my previous post, so it doesn't cause any confusion... |
@cur3n4 thanks for the pointers, I've been busy so haven't been able to get to this until now. @jeff-kilbride delighted it works for you and you can chop out some extra code! Doing the same in my project now 😄 |
So I have Hapi 16.5.2 when is this going to be included? |
It's been included since 16.4.0. |
This thread has been automatically locked due to inactivity. Please open a new issue for related bugs or questions following the new issue template instructions. |
This small change allows an extra degree of protection for those who might be using promises in their handlers.
Problem Statement
Developers who are using promises in their handlers will often write handlers which look like this:
The challenge is that this opens up lots of opportunities for exceptions at different points to lead to uncaught promise rejections. For example:
will lead to a timeout of the request.
Suggested Change
Rather than attempting some kind of large scale change which adds more support for promises (which is not needed, as
reply
can take a promise), I propose allowing the handler to return a promise. If the promise is returned, then hapi can register acatch
andreply
with any error. This means that if the developer goofs up and forgets to catch (all to easy with the promise spec), hapi at least will attempt to deal with the error in the same way it would as if an exception was thrown.This PR adds the support, updates the docs and tests.
Questions
Do we need this?
It is possible to rewrite the dodgy code as:
However this tends to feel a little unnatural, as the natural reading order is more 'do work, reply with the result'. Fairly subjective but I've seen quite a few devs on teams default to doing it the way described in the problem statement, rather than this. It also makes doing things like changing the status code conditionally based on the result near impossible. (CMIIW!)
Of course, people could just avoid promises (which might not be a bad idea given some of the issues around how they can swallow exceptions), but at least an appreciable proportion of people are using them heavily.
Should we ever have 'partial promise' support
Really, if you are returning a promise in the handler it might make more sense to simply not even have the
reply
function used, and change the spec to sayif you return a promise, hapi with reply with the result or if there is an error, wrap it in Boom and then reply with that
.This would perhaps be cleaner in some cases:
But is a larger structural change. Also, it still means we cannot easily change status codes, add headers etc.
I think this is a happier middle group - hapi's API stays the same, but it has a 'safety net' for promises.
That's It!
I'd love to get any feedback on this as a potential feature. It would certainly help myself and my colleagues on some of the projects we're on at the moment, I believe it allows us to provide a safety net for potentially nasty issues (promise black holes are a pain) but am happy to take any input for changes!
Related Issues