-
Notifications
You must be signed in to change notification settings - Fork 1.7k
proposal: avoid_future_catcherror
#59040
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
Comments
In addition I created a draft PR for this: https://github.com/dart-lang/linter/actions/runs/4177158649/jobs/7234383340 I used it to create the following Flutter CLI tool PR flutter/flutter#120637 |
I don't think this behavior is restricted to void main() {
List<Object?> list = <bool>[true, false];
list.add('hello');
} This code also always has a runtime type error, but no static error. I suspect it would be a mistake to implement such checks piecemeal. |
We certainly don't want to do so without due consideration. To start the conversation, one question that I'd want to ask is how many false positives would a general lint have compared to a more specific lint. I haven't thought about it long enough to have an answer, but someone else might have. |
What would a more general lint look like? The only thing I can think of would be to warn when assigning a |
As an aside, it sounds like @lrhn is going to update |
….catchError (#140122) Ensure tool code does not use Future.catchError or Future.onError, because it is not statically safe: dart-lang/sdk#51248. This was proposed upstream in dart-lang/linter in https://github.com/dart-lang/linter/issues/4071 and dart-archive/linter#4068, but not accepted.
@srawlins so, |
@lrhn What do you think? Is void f(Future fut) {
- fut.catchError((e, s) {});
+ fut.onError((e, s) {});
+ // Add a dead parameter.
- fut.catchError((e) {});
+ fut.onError((e, _) {});
void callback(Object e) {}
+ // Wrap a non-function-expression.
- fut.catchError(callback);
+ fut.catchError((e, _) => callback(e));
} Such that If there are any reasons, or enough reasons, to keep Also cc @iinozemtsev who has been looking at similar issues internally, IIRC. |
Switching to using Changing existing catches sound be OK if possible. The main difference is that
There are also some type checks that aren't done until the error has been caught. To make those type statically, we might have to widen the type the handler accepts. If we can do a type based migration, this should all be possible. A few might need a dynamic downcast. |
import 'dart:math';
void main() async {
// Introduce covariance: Static and dynamic type arguments differ.
Future<Object> future = Future<int>.error("Ouch!");
var result = Random().nextBool()
? future.catchError((_, __) => 42)
: future.onError((_, __) => 42);
if (result is Future<int>) {
print('Future<int>'); // Will arrive here if we used `catchError`.
} else {
print('Future<Object>'); // Will arrive here if we used `onError`.
}
} It could be argued that this example is unrealistic, because we implicitly claim to have "forgotten" that the type argument of the future is However, it is actually possible to align the actual type argument of the future with the value which is used in case of an error, and even the function object can have the correct return type ( One way to do this is to bundle the future and the value together at a location in the code where the future has just been created (such that we know the precise type argument and we can choose a value of that type, or provide some devices that will allows us to obtain such a value when needed). OK, developers probably aren't going to do this in real life, but it illustrates that it is possible to use techniques whereby the type safety is guaranteed even in the presence of covariance (as long as we use that technique consistently). import 'dart:math';
class Bundle<X> {
final Future<X> future;
final X value;
Bundle(this.future, this.value);
X onError(_, __) => value;
}
void main() async {
// Introduce covariance: Static and dynamic type arguments differ.
Bundle<Object> bundle = Bundle<int>(Future.error("Ouch!"), 42);
var result = Random().nextBool()
? bundle.future.catchError(bundle.onError)
: bundle.future.onError(bundle.onError);
if (result is Future<int>) {
print('Future<int>'); // Will arrive here if we used `catchError`.
} else {
print('Future<Object>'); // Will arrive here if we used `onError`.
}
} The point is simply that if somebody insists on doing something like this then perhaps they wouldn't want to switch from |
Ideally I would love to be able to ban all covariant assignments for certain types (like Completer, Future, Sink), but I'm also voting with both hands for the ability to ban The tricky behavior is indeed possible, but I think this just means that automated fixes should be carefully reviewed (and it would still be possible to ignore this lint via |
@eernstg There are very few situations where you want to store a |
@lrhn wrote:
We could have a long style discussion about this. I was just pointing out the fact that if a future is typed covariantly (that is, the statically known actual type argument at I'm sure we can come up with a hundred reasons why most futures aren't typed covariantly, but I'd still claim that it is plausible that some futures are typed covariantly in a reasonably well designed Dart program. Due to the complexity of some applications it is not quite fair to say "it might as well do that check before the branch" (meaning "reasonably well designed Dart code will never encounter a situation where a future is typed covariantly, and the run-time type argument of that future does matter for the application logic"). So let's consider the situation based on the premise that we have encountered a covariantly typed future whose run-time type does matter to the application logic. In that situation, the semantics of
You can use many different coding techniques to obtain a value of a type which is only known by an upper bound, e.g., you could use a factory or keep an actual value around. I used the latter because it's very simple to express, and the actual bundling into a separate instance of type
You're just restating the fact that an instance member has a special power, namely the ability to denote (and hence use) a given type parameter. So that's true, but the whole point of this discussion is that So your advice is good, but it can be unrealistic. Anyway, as I mentioned, it is definitely possible that the scenario I described will be considered esoteric and we should just ignore it. However, we could also take it into account and mention in the lint message emitted by |
I think we should ignore the difference, and I support using Using the static type instead of the future's inherent type is difference, but that's a benficial feature in most cases. (I sometimes wish I could have the effect of extension methods, using the static type of the receiver, in instance methods.) |
avoid_future_catcherror
Description
Avoid calling Future.catchError.
Details
Calling the
catchError
method on a Future can lead to type errors at runtime if theruntime type of the Future is more specific than that of the variable or argument that references it.
Kind
Does this enforce style advice? Guard against errors? Other?
Bad Examples
Good Examples
Discussion
This is the SDK issue indicating tracking that statically correct usage of Future.catchError can lead to a runtime ArgumentError: #51248. This investigation was initiated by flutter/flutter#114031, which was a runtime crasher in the Flutter CLI tool that was very difficult to track down.
I intend to implement this lint in the Flutter CLI tool, and will discuss enabling it across the Flutter SDK.
The "Bad example" above will not hit this runtime issue. Should I have included an example that would crash at runtime? A trivial example of this would be:
Discussion checklist
The text was updated successfully, but these errors were encountered: