-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Casting Future<Foo>
to Future<Foo?>
leads to hard-to-debug errors too easily
#47308
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
We're not testing the arguments to That also prevents us from giving the error inside Adding earlier and stricter checks is definitely possible, but also very likely to break existing code. |
Can the closure "remember" the location in code where it was defined, so when there's need to complain about it the error message could point to it. Example:
|
The code definitely already knows that the function came from either Remembering where it came from is a much tougher request. We could capture the stack trace for every Maybe we could capture the stack trace only in debug mode (capture and assign it inside an Or we could wrap the error handler function where it's provided, maybe only if it doesn't have the correct type, and let the wrapper do the type checking. Then it failing would throw in code that was, well, just inside |
IIRC the DevTools team implemented a kernel transformer that remembers where widgets are instantiated. This way there's no overhead at runtime as the location is hard-coded in the compiled code. Can the same approach be used here? Also, while the full stack could be even more helpful, just the source location alone would already help a lot. |
The source location is in user code, so we can't capture that in Dart code. Honestly, I'd much rather breakingly change the API to only accept (Would it be meaningful to have a lint checking that the on-error handler has the required type? It'll probably end up being impractical to satisfy the return type for a function literal without a helpful context type.) |
I was thinking of a compile-time solution rather than a runtime one. Instead of checking the stack at runtime, the compiler could attach the line information to the closure/tear-off. Then at runtime the line information can be read back and printed as part of the exception (perhaps I'm not sure how a lint can help. The static types are correct. The problem is with the |
Yes, casting Until we get variance annotations, up-casts of classes that are not completely covariant are always going to be risky. If it was only the up-cast that was a problem, but it's really every use of Accepting So, checking the actual value is currently the best we can do, and we do that. At least we check the error handler callback parameter types eagerly. I don't think there is anything more we can do here, short of a breaking change. |
What do you think of a compiler-based solution, where one can extract the source location of the closure/tear-off instantiation using something like |
The solution to unsound covariance is variance annotations, which we don't have yet, and likely won't be able to attach to existing technically-invariant SDK types anyway. Attaching location information to errors related to function values is doable. Heck, I'd let the VM put the location information directly into the function's |
The statically checked declaration-site feature (cf. dart-lang/language#524) is not fully implemented, but much of it is there already. How about offering a tiny preview of this feature: We could have an annotation
This is not the complete declaration-site variance feature, not even for the special case where every type parameter of a specific class would have the variance modifier That lint could be enabled during debugging of the kind of problem described here. The lint might even be enabled permanently in certain libraries where futures are used in an invariant manner. But it might also be used in a debugging-only manner. That's definitely not going to break anything, so we don't have to worry about questions like "would it break the world to make It is not sound, of course, so we still need the actual feature in order to get the real thing: Y f<X extends Y, Y>(X x) => x;
void main() {
Future<int?> fut = f(Future<int>.value(42)); // No lint.
} With real declaration-site variance, and assuming that |
This is not urgent, at least not for anything I'm working on. So if dart-lang/language#524 covers it, it's fine to wait for that feature to land. Not sure if |
I'd personally go a long way towards making The things that prevent it from being covariant today are:
In almost all cases, and certainly the stream ones, having super-bounded type parameters could save the day. If we have a covariant occurrence of |
That would be nice! Cf. dart-lang/language#1674. ;-) |
To reproduce
dart run
the following code:Expected result
The stack trace should point to the code containing the programming error, i.e. (
future.catchError
). Consider the following non-async code containing a similar error:The result is:
The stack trace points to the line 4 in the file
null_future.dart
that I wrote, which is helpful.Actual result
This stack trace is not helpful at all. The best method I could find is pause on exceptions in the debugger, wait until it pauses on this line, then eval
$T
. This gives you the type that it expected. Then you grep your source code for futures and completers that use this type (hopefully there aren't too many places) and try to spot the bug.As a concrete example, here's the bug in the Flutter Web engine tool, which was a conspiracy of three separate lines of code:
Completer<BrowserEngine>
instantiated..future
is implicitly cast toFuture<BrowserEngine?>
.catchError
attempts to return null.No matter what Dart program you write you see the exact same stack trace, making it very hard to debug.
The text was updated successfully, but these errors were encountered: