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
New: prefer-promise-reject-errors
rule (fixes #7685)
#7689
Conversation
@not-an-aardvark, thanks for your PR! By analyzing the history of the files in this pull request, we identified @IanVS, @mysticatea and @doberkofler to be potential reviewers. |
if (resolveRejectFuncs.has(node)) { | ||
|
||
// Get all variables in the function's scope. | ||
context.getScope().variables |
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 think we can use context.getDeclaredVariables(node)
instead of context.getScope().variables
to simplify the finding logic (I guess WeakSet
s are unnecessary).
If the node
is a function, context.getDeclaredVariables(node)
returns an array which includes the variables of the function name and parameters.
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.
Left a few thoughts, but this looks pretty good so far.
|
||
Due to the limits of static analysis, this rule cannot guarantee that you will only reject Promises with `Error` objects. While the rule will report cases where it can guarantee that the rejection reason is clearly not an `Error`, it will not report cases where there is uncertainty about whether a given reason is an `Error`. For more information on this caveat, see the [similar limitations](http://eslint.org/docs/rules/no-throw-literal#known-limitations) in the `no-throw-literal` rule. | ||
|
||
To avoid conflicts between rules, this rule does not report non-error values used in `throw` statements in async functions, even though these lead to Promise rejections. To lint for these cases, use the [`no-throw-literal`](http://eslint.org/docs/rules/no-throw-literal) rule instead. |
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.
Minor not: Consider removing "instead" at the end of this line, which may falsely imply that these rules have no point in being enabled together.
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.
s/not/nit, sorry.
if ( | ||
!callExpression.arguments.length || | ||
!astUtils.couldBeError(callExpression.arguments[0]) || | ||
callExpression.arguments[0].type === "Identifier" && callExpression.arguments[0].name === "undefined" |
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'm a bit concerned about this logic- undefined can be redefined, and further other variables could be declared-but-not-defined for the same issue. Maybe it would be better to use escope to try to find a definition with no initialization instead?
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.
As far as I know, other rules also do not check shadowing of the undefined
variable to cover the old well-known pattern:
;(function(undefined) {
// do something
}())
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.
@mysticatea I have no intention of making that pattern unusable. I just want to use escope to check that whatever variable is used is definitely undefined (in value), rather than looking for the name "undefined".
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 added this check because no-throw-literal
also has a special case for it (see here).
Personally I think the rule is better with the check, because undefined
is almost always going to be the same as not passing any argument to Promise.reject
. In my opinion, this is one of those cases where if you do weird things such as creating an Error object called undefined
and rejecting a Promise with it, you're on your own and the rule shouldn't become worse for the majority of undefined
cases just to handle that outlier.
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 don't see this as a blocker- let's not change this. I'll follow up with a new issue later.
if ( | ||
|
||
// Check `Promise.reject(value)` calls | ||
node.callee.type === "MemberExpression" && |
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.
Can all this be extracted to one or more helper functions?
"foo && 2": true, | ||
"foo ? 1 : 2": false, | ||
"foo ? bar : 2": true, | ||
"foo ? 1 : bar": true |
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.
Do we want to add object expressions here?
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 assume you're referencing the note at the top of this PR. What do you think the function should return for object expressions?
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.
False, generally (my assumption is the Error constructor crafts the stack trace, so an object with a message
or what have you will usually not suffice).
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 would be fine with that behavior, but I'm wondering if it would cause confusion because the rule is called no-reject-literal
and object expressions are not literals.
Also, I think that would be a breaking change for no-throw-literal
, (unless we explicitly made no-throw-literal
more lenient by allowing ObjectExpressions
).
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.
One possible documentation issue, otherwise LGTM.
@@ -0,0 +1,57 @@ | |||
# disallow using literals as Promise rejection reasons (no-reject-literal) | |||
|
|||
It is considered good practice to only instances of the built-in `Error` object for user-defined errors in Promises. `Error` objects automatically store a stack trace, which can be used to debug an error by determining where it came from. If a Promise is rejected with a non-`Error` value, it can be difficult to determine where the rejection occurred |
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 don't see a period at the end of this line, but it could be my display. Disregard if this is inaccurate.
if ( | ||
!callExpression.arguments.length || | ||
!astUtils.couldBeError(callExpression.arguments[0]) || | ||
callExpression.arguments[0].type === "Identifier" && callExpression.arguments[0].name === "undefined" |
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 don't see this as a blocker- let's not change this. I'll follow up with a new issue later.
LGTM |
I can respect the possible breaking change argument. No need to change this.
…On Dec 4, 2016 3:38 PM, "Teddy Katz" ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In tests/lib/ast-utils.js <#7689>:
> + foo: true,
+ "new Foo": true,
+ "Foo()": true,
+ "foo`bar`": true,
+ "foo.bar": true,
+ "(foo = bar)": true,
+ "(foo = 1)": false,
+ "(1, 2, 3)": false,
+ "(foo, 2, 3)": false,
+ "(1, 2, foo)": true,
+ "1 && 2": false,
+ "1 && foo": true,
+ "foo && 2": true,
+ "foo ? 1 : 2": false,
+ "foo ? bar : 2": true,
+ "foo ? 1 : bar": true
I would be fine with that behavior, but I'm wondering if it would cause
confusion because the rule is called no-reject-literal and object
expressions are not literals.
Also, I think that would be a breaking change for no-throw-literal,
(unless we explicitly made no-throw-literal more lenient by allowing
ObjectExpressions).
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#7689>, or mute the thread
<https://github.com/notifications/unsubscribe-auth/AARWen-1ezY5fjFRGGs-h-PD03ms1vU3ks5rEzLTgaJpZM4LDLYz>
.
|
I'll add the ObjectExpression case to our 4.0.0 to-do list. |
@mysticatea I noticed you left a comment on this PR (which I think I've addressed), but you didn't leave a "Request Changes" review initially. Do you want to review this PR again, or do you think it's ready to be merged? (There's no rush, but I don't want to merge it if you still have changes to suggest.) |
|
||
Promise.reject(5); | ||
|
||
Promise.reject(); |
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.
This should be totally fine - rejecting with a literal is a problem, but rejecting with nothing (simply to be able to trigger a .catch
) is totally legit imo.
At the very least, could this be an option, so that I can use this rule while allowing Promise.reject()
?
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.
Could you elaborate on your use case for Promise.reject()
? In a stack trace that would show up as Uncaught undefined
, which isn't very useful.
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.
My use case is when I'm using promises as flow control, and I don't care about the rejection reason.
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.
But if it's in a .then
call, wouldn't throw
accomplish the same thing?
To me, it seems like if you're using rejected Promises for something other than error handling, you would probably want to disable this rule anyway. But maybe I'm still misunderstanding the use case.
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.
There are plenty of use cases where you want to send users down the .catch
path without providing an error reason, but your codebase will still have many places where you do care about the error reason, and would want to ensure that no literals are used.
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.
There are many parallels between promises and syntactic try/catch, indeed. Promises, however, are more powerful than that, and don't have to be limited to try/catch. What's the Promise.all
or Promise.race
parallel in try/catch?
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 feel like we've gotten a bit off-topic. I'd be glad to keep discussing the different things that Promises can be used for and their synchronous parallels, but maybe we should do that e.g. in a chatroom so that we don't spam peoples' inboxes.
At the moment, I'm still leaning towards 👎 for adding an exception for undefined
as Promise rejection reasons, but I'm fine with waiting to see what the rest of the team thinks about it. (Alternatively, maybe it would be better to open a separate issue after this rule lands on master, since it would be a backwards-compatible change.)
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 agree with @not-an-aardvark .
I don't think that this rule should allow undefined
.
I felt the smell like the following code. It would ignore true errors also.
try {
list.forEach(x => {
if (x == null) {
// this is an abuse of exceptions.
throw null
}
doSomething(x)
})
} catch (_) {}
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.
@mysticatea note that I'm not saying it should allow Promise.reject(undefined)
- I'm saying that that is not at all the same as Promise.reject()
because the latter has zero arguments, and the former one.
More specifically, Promise.reject()
has no literals of any kind in it - so why is this rule, "no reject literal", concerned with it?
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.
As the result, error handlers will 100% receive undefined
.
It's one of what this rule want to prevent.
Promise.reject(); | ||
|
||
new Promise(function(resolve, reject) { | ||
reject("something bad happened"); |
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.
new Promise(function (resolve, reject) {
throw 'something bad';
});
this will reject too - should it be left for no-throw-literal
, or should this rule cover it also?
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.
no-throw-literal
already reports this, so I think it's probably best to not report it to avoid conflicts.
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.
LGTM, thank you!
Regarding the conversation around To me, the rule is "When rejecting with an argument, it should be an Error so that a stack trace can be used". This is the same as no-throw-literal: "When throwing something, it should be an Error so that a stack trace can be used". But you can't throw nothing. Even if my reasoning is not persuasive, I'd like to call out an element of ESLint philosophy, which is this: ESLint is not opinionated. To me, forbidding |
This rule and
But
We do allow users to opt in; they can decide whether or not to enable this rule in their config. But once a user has opted into reporting non-Error rejection reasons, having the rule report |
To expand on this a bit: I acknowledge that the rule name is a bit misleading. But the documentation for
This is why the rule currently reports statements like this: throw (condition ? "foo" : "bar"); Clearly, the expression Similarly, the argument (or lack thereof) in Based on the confusion in this thread, I'd like to propose two changes to this rule:
|
I'm still not ultimately convinced that this rule should be an exact parallel to no-throw-literal. We should not be the ones who decide best practices, but rather we should defer to the community when the practices aren't objectively wrong. Thus I still think @ljharb's points deserve further consideration and should not be rejected (heh) outright. It seems to me that if promise rejections are not the same as errors, we should not treat them the exact same in these rules (or at least, should provide options for reasonable uses). I'm sorry to do this, but I'm now 👎 on this proposal until we can come up with something reasonable to satisfy @ljharb's use case. |
I think I've given them consideration, though. The point of the proposal is to ensure that promise rejections always have stacktraces, to avoid confusing errors like this. @ljharb's use case explicitly creates Promise rejections that don't have stack traces, which goes against the purpose of the rule. I don't see a need to satisfy that use case in this rule, because it gets rid of the entire benefit that would be gained by enabling the rule. Someone who wants to use Promises like that can just as easily disable this rule. If we want to have a discussion about why using Error values as Promise rejection reasons is a good practice, then I'm certainly open to doing that. But I don't think we should make this rule worse by adding an exception for code that completely goes against what the rule is trying to enforce. |
I want to be able, in the same codebase without monkeying with override comments, have |
So, is this code invalid? function foo(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, content) => {
if (err) {
reject(err)
return
}
doSomething()
})
})
} function foo(name) {
return new Promise((resolve, reject) => {
const cp = child_process.spawn(name)
cp.on("error", (err) => {
doSomething()
reject(err)
})
})
} |
Good point - rejecting with the error from a nodeback is a primary use case of |
It sounds fair enough to me. |
That would prevent things like this: function createError() {
if (someCondition) {
return new FooError(args);
} else {
return new BarError(args);
}
}
function businessLogic() {
if (condition) {
return doSomethingAsync();
} else {
return Promise.reject(createError());
}
} But I would be fine with an option for strict reporting (where the rule reports something if it's possibly wrong, rather than certainly wrong). My main problem is allowing |
My position is:
|
eslint has rules that allow lots of bad practices, and leaves it up to the user whether to enable or configure the rules as such. In the pattern If I'm explicitly enabling an option that allows |
TSC Summary: TSC Question: Should we add an option to allow |
Per TSC meeting: We agreed to add a new option that would be off by default and would allow |
As @ilyavolodin mentioned, the TSC resolution was to add an option for this, so I'll work on doing that. Does anyone have thoughts on the two bullet points at the bottom of #7689 (comment)? I think the rule might be less confusing if we change the name. |
Since no one objected to the comment above, I'm planning to update this PR with the following changes:
|
ee97998
to
377a20a
Compare
LGTM |
no-reject-literal
rule (fixes #7685)prefer-promise-reject-errors
rule (fixes #7685)
Ok, I made the changes I described above. This is ready to be reviewed again. I squashed the commits to make sure the commit message with the correct name makes it into the changelog. For reviewing convenience, here is a copy of the changes since the last review. |
.filter(ref => ref.isRead()) | ||
|
||
// Only check the references that are used as the callee in a function call, e.g. `reject(foo)`. | ||
.filter(ref => ref.identifier.parent.type === "CallExpression" && ref.identifier === ref.identifier.parent.callee) |
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.
Nit: Just curious if this filter and above filter can be combined into single filter statement?
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.
They could be -- do you think that would be better? Personally I think the code is clearer with the filters separated, but I'd be happy to change it if you feel differently.
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 can probably get the best of both worlds by extracting both of those filters into a single predicate function and then using that function name, if you're worried about readability and you want to follow gyandeeps' suggestion.
What is the purpose of this pull request? (put an "X" next to item)
[x] New rule
See #7685
What changes did you make? (Give an overview)
This adds the
no-reject-literal
rule as discussed in #7685, and enables it in the ESLint repo for dogfooding.Some of the logic from this rule is shared with
no-throw-literal
, so that logic has been moved out ofno-throw-literal
and intoast-utils.js
.Is there anything you'd like reviewers to focus on?
I'm wondering whether we should have the rule report object expressions, e.g.
Advantages of reporting that case:
{ foo: 1 }
cannot be an instance ofError
.Disadvantages of reporting that case:
{ foo: 1 }
is not actually a literal, so the name of the rule might end up a bit misleading.no-throw-literal
, which does not reportthrow {foo: 1}
.The current implementation does not report object expressions.